diff --git a/.vscode/mcp.json b/.vscode/mcp.json index a808733..600e2de 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -6,14 +6,21 @@ }, "ESLint": { "type": "stdio", - "command": "npx", - "args": ["@eslint/mcp@latest"] + "command": "pnpm", + "args": [ + "dlx", + "--package=jiti", + "--package=@eslint/mcp@latest", + "-s", + "mcp" + ] }, "io.github.upstash/context7": { "type": "stdio", - "command": "npx", + "command": "pnpm", "args": [ - "-y", + "dlx", + "-s", "@upstash/context7-mcp@latest", "--api-key", "${input:CONTEXT7_API_KEY}" @@ -21,26 +28,13 @@ }, "io.github.ChromeDevTools/chrome-devtools-mcp": { "type": "stdio", - "command": "npx", - "args": ["chrome-devtools-mcp@0.10.2"] - }, - "oraios/serena": { - "type": "stdio", - "command": "uvx", - "args": [ - "--from", - "git+https://github.com/oraios/serena", - "serena", - "start-mcp-server", - "serena==latest", - "--context", - "ide-assistant" - ] + "command": "pnpm", + "args": ["dlx", "-s", "chrome-devtools-mcp@0.10.2"] }, "socket-mcp": { "type": "stdio", - "command": "npx", - "args": ["@socketsecurity/mcp@latest"], + "command": "pnpm", + "args": ["dlx", "-s", "@socketsecurity/mcp@latest"], "env": { "SOCKET_API_KEY": "${input:socket_api_key}" } diff --git a/.vscode/settings.example.json b/.vscode/settings.example.json index 02ff469..7bf29b2 100644 --- a/.vscode/settings.example.json +++ b/.vscode/settings.example.json @@ -7,5 +7,6 @@ "svelte.language-server.runtime": "node", "eslint.validate": ["javascript", "typescript", "svelte"], "typescript.tsdk": "./node_modules/typescript/lib", - "typescript.enablePromptUseWorkspaceTsdk": true + "typescript.enablePromptUseWorkspaceTsdk": true, + "typescript.tsserver.experimental.enableProjectDiagnostics": true } diff --git a/AGENTS.md b/AGENTS.md index 2db9ea2..ddafa9b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -183,39 +183,6 @@ Lints specified files and returns errors/warnings. Use this to validate your cod **Usage:** `mcp_eslint_lint-files` with an array of absolute file paths. -### 4. Serena MCP Server (`mcp_oraios_serena_*`) - -Intelligent code navigation and symbolic editing: - -#### Code Exploration - -- `get_symbols_overview` - Get high-level view of symbols in a file (use this FIRST) -- `find_symbol` - Find and read specific symbols by name path -- `find_referencing_symbols` - Find where symbols are used -- `search_for_pattern` - Flexible regex-based code search - -#### Code Editing - -- `replace_symbol_body` - Replace entire symbol body (methods, classes, etc.) -- `insert_after_symbol` - Insert code after a symbol -- `insert_before_symbol` - Insert code before a symbol -- `rename_symbol` - Rename symbols throughout codebase - -#### Memory Management - -- `write_memory` - Save project information for future reference -- `read_memory` - Retrieve saved project context -- `edit_memory` - Update existing memories -- `delete_memory` - Remove outdated memories - -#### Project Management - -- `activate_project` - Switch between registered projects -- `get_current_config` - View current configuration - -> [!TIP] -> Use symbolic tools to read only necessary code. Start with `get_symbols_overview` before reading full files. - ### 5. Socket MCP Server (`mcp__extension_so_depscore`) Dependency security and quality scoring: @@ -504,14 +471,14 @@ Since Svelte 5.25, you can reassign `$derived` values to temporarily override th When using SvelteKit remote functions, you can use `.updates()` with `.withOverride()` to optimistically update the cache of a query. ```typescript -import { getPosts, createPost } from '$lib/remote/posts.remote'; +import { getPosts, createPost } from "$lib/remote/posts.remote"; async function handleSubmit() { - const newPost = { id: 'temp', title: 'New Post' }; + const newPost = { id: "temp", title: "New Post" }; // Optimistically update the getPosts query cache await createPost(newPost).updates( - getPosts().withOverride((posts) => [newPost, ...posts]) + getPosts().withOverride((posts) => [newPost, ...posts]), ); } ``` diff --git a/SHARING.md b/SHARING.md new file mode 100644 index 0000000..6d22bff --- /dev/null +++ b/SHARING.md @@ -0,0 +1,350 @@ +# **Federated Notes App — Structure, Permissions, and Sharing Spec** + +## **Status**: + +**_Draft_** + +# Please Approve or Edit + +--- + +## **1. Core Design Invariants** + +The system MUST obey the following rules at all times: + +1. Spaces define visibility +2. Visibility has exactly one source +3. Notes never define visibility +4. Per-note permissions restrict actions only +5. Structure never implies permission +6. Permission never implies structure +7. One space has exactly one authority(server in the federated system) of record + +Any feature that violates one of these invariants is invalid. + +--- + +## **2. Spaces** + +### **2.1 Definition** + +A **Space** is the only internal visibility and collaboration boundary. + +### **2.2 Properties** + + Space + - space_id + - authority_id + - members: Map + - root_folder_id + +### **2.3 Rules** + +- Every space has exactly one authority of record +- Spaces cannot be nested +- Spaces may include members from multiple federations +- All notes in a space are owned by the space authority +- All space members can see all notes in the space +- Spaces may not include notes owned by other authorities + +### **2.4 Purpose** + +Answers the question: + +> Who can see all content here by default? + +--- + +## **3. Folders (Within Spaces)** + +### **3.1 Definition** + +Folders are structural containers inside a space. + +### **3.2 Rules** + +- Folders inherit space visibility +- Folders may be nested arbitrarily +- Folders cannot be shared +- Folders cannot alter permissions +- Folders cannot contain notes from other spaces + +### **3.3 Purpose** + +Answers the question: + +> How is content organized within the space? + +--- + +## **4. Notes** + +### **4.1 Definition** + +A **Note** is the smallest unit of content and the smallest externally shareable unit. + +### **4.2 Properties** + + Note + - note_id + - space_id + - authority_id + - content + - metadata + +### **4.3 Rules** + +- A note belongs to exactly one space +- A note is visible to all space members +- A note may be included in zero or more Share Groups +- A note may have per-note action restrictions +- A note cannot exist in multiple spaces simultaneously + +--- + +## **5. Per-Note Action Permissions** + +### **5.1 Definition** + +Per-note permissions restrict **actions**, not visibility. + +### **5.2 Allowed Restrictions** + +- Read +- Comment +- Suggest +- Edit +- Lock +- Prevent delete +- Prevent move +- Prevent rename + +### **5.3 Forbidden Capabilities** + +- Restricting visibility +- Adding new viewers +- Creating sub-audiences +- Partial space visibility + +### **5.4 Resolution Order** + + Effective Action Permission = + least_privileged( + External Share Role (if applicable), + Note Policy, + Space Role + ) + +### **5.5 Rule** + +If a user can see a note, per-note policies only determine what they may do to it. + +--- + +## **6. Collections** + +### **6.1 Definition** + +Collections are **local, per-user organizational folders for spaces**. + +### **6.2 Rules** + +- Collections are client-side only +- Collections are not shared +- Collections have no permissions +- Collections do not affect visibility +- Collections are not part of URLs or linking + +### **6.3 Invariant** + +Removing collections does not affect collaboration or access. + +--- + +## **7. File Tree** + +### **7.1 Purpose** + +The file tree displays structural truth only. + +### **7.2 Displays** + +- Spaces +- Folders +- Notes + +### **7.3 Does Not Display** + +- External share groupings +- Permission bundles +- Visibility differences + +### **7.4 Annotations** + +Notes may display informational badges indicating: + +- External sharing +- Locked or restricted actions + +These annotations do not alter structure. + +--- + +## **8. External Shares** + +## **9\. Share Groups** + +### **9.1 Definition** + +An **is the only wat\* y to share a norte mm, re ,,h o outside of a space.f +A **Share Group\*\* is a first-class permission object representing an external share. + +### **9.2 Properties** + + ShareGroup + - share_group_id + - owner_space_id + - note_ids[] + - user_ids[] + - role (viewer | commenter | editor) + +### **.3 Rules** + +- Only listed notes are visible +- Only listed users receive access +- No inheritance +- No structure +- Not represented in the file tree + +--- + +## **10\. Visibility Resolution (Authoritative)** + +A user may see a note if and only if: + + user ∈ space.members + OR + (user ∈ share_group.users AND note ∈ share_group.notes) + +Exactly one visibility source applies. + +--- + +## **11\. External User Experience** + +### **11.1 Entry Point** + +. +**Shared with Me** + +### **11.2 Display Model** + +Each Share Group appears as a grouped entry. + + Shared with Me + Project – Teacher Review + - notes.md + - research.md + - summary.md + +### **11.3 Restrictions** + +External users: + +- Cannot browse spaces or folders +- Cannot see sibling notes +- Cannot resolve non-shared links +- Cannot view backlinks or graphs + +--- + +## **12\. Private Notes** + +### **12.1 Definition** + +A private note is a note in a single-user space. + +### **12.2 Sharing** + +Private notes are shared exclusively via Share Groups. + +There is no separate private-sharing mechanism. + +--- + +## **13\. Moving Notes Between Spaces** + +. + +### **13.1 Definition** + +Moving a note between spaces changes its ownership and visibility. + +### **13.2 Same-Authority Move** + +- space_id is updated +- note_id remains unchanged +- Note becomes visible to destination space members +- All external shares are revoked + +### **13.3 Cross-Authority Move** + +- Conte. is exported +- New note is created under the destination authority +- New note_id is assigned +- Original note may be deleted explicitly +- Links are rewritten where possible + +### **13.4 UX Requirement** + +Users must be warned on move: + +> Moving this note will make it visible to all members of the destination space. +> External shares will be removed. + +--- + +## **15\. Explicitly Disallowed** + +The system will never support: + +- Nested spaces +- Folder-level sharing +- Structure-derived permissions +- Partial visibility inside a space +- Cross-authority notes within a space +- Implicit sharing +- Share Groups as navigable containers + +--- + +## **16\. Model Summary** + +. +**Layer** + +**Responsibility** + +Spaces + +Visibility and ownership + +Folders + +Structure + +Notes +. +Content + +Note Policies + +Action restrictions + +Collections + +Personal navigation + +Share Groups + +Explicit visibility exceptions diff --git a/package.json b/package.json index eb07953..e494b7a 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "dev": "vite dev", + "dev:federation": "./scripts/start_federation.sh", "build": "vite build", "preview": "vite preview", "prepare": "svelte-kit sync", @@ -25,7 +26,7 @@ "@oslojs/crypto": "^1.0.1", "@oslojs/encoding": "^1.1.0", "@sveltejs/adapter-auto": "^7.0.0", - "@sveltejs/kit": "^2.49.0", + "@sveltejs/kit": "^2.49.2", "@sveltejs/vite-plugin-svelte": "^6.2.1", "@tailwindcss/typography": "^0.5.19", "@tailwindcss/vite": "^4.1.17", @@ -35,6 +36,7 @@ "eslint": "^9.39.1", "eslint-plugin-svelte": "^3.13.0", "globals": "^16.5.0", + "jiti": "^2.6.1", "playwright": "^1.56.1", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", @@ -63,6 +65,8 @@ "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.8", "@effect/platform": "^0.93.3", + "@hpke/chacha20poly1305": "^1.7.1", + "@hpke/core": "^1.7.5", "@lezer/highlight": "^1.2.3", "@lezer/markdown": "^1.6.0", "@lucide/svelte": "^0.554.0", @@ -75,7 +79,6 @@ "@milkdown/preset-gfm": "^7.17.1", "@milkdown/theme-nord": "^7.17.1", "@milkdown/utils": "^7.17.1", - "@modelcontextprotocol/sdk": "^1.22.0", "@node-rs/argon2": "^2.0.2", "@prosemark/core": "^0.0.4", "@prosemark/paste-rich-text": "^0.0.2", @@ -90,10 +93,12 @@ "daisyui": "5.5.5", "dotenv": "^17.2.3", "effect": "^3.19.6", - "fast-diff": "^1.3.0", "katex": "^0.16.25", + "loro-codemirror": "^0.3.3", "loro-crdt": "^1.10.0", - "svelte": "^5.44.0", + "runed": "^0.37.0", + "svelte": "^5.45.8", + "temporal-polyfill": "^0.3.0", "tiptap-markdown": "^0.9.0", "typescript-svelte-plugin": "^0.3.50" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bf2bbfc..7b062e3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 1.1.0 '@benrbray/prosemirror-math': specifier: ^1.0.0 - version: 1.0.0(katex@0.16.25)(prosemirror-commands@1.7.1)(prosemirror-history@1.5.0)(prosemirror-inputrules@1.5.1)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-transform@1.10.5)(prosemirror-view@1.41.3) + version: 1.0.0(katex@0.16.27)(prosemirror-commands@1.7.1)(prosemirror-history@1.5.0)(prosemirror-inputrules@1.5.1)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-transform@1.10.5)(prosemirror-view@1.41.4) '@codemirror/autocomplete': specifier: ^6.20.0 version: 6.20.0 @@ -46,55 +46,58 @@ importers: version: 6.1.3 '@codemirror/view': specifier: ^6.38.8 - version: 6.38.8 + version: 6.39.0 '@effect/platform': specifier: ^0.93.3 - version: 0.93.3(effect@3.19.6) + version: 0.93.6(effect@3.19.9) + '@hpke/chacha20poly1305': + specifier: ^1.7.1 + version: 1.7.1 + '@hpke/core': + specifier: ^1.7.5 + version: 1.7.5 '@lezer/highlight': specifier: ^1.2.3 version: 1.2.3 '@lezer/markdown': specifier: ^1.6.0 - version: 1.6.0 + version: 1.6.1 '@lucide/svelte': specifier: ^0.554.0 - version: 0.554.0(svelte@5.44.0) + version: 0.554.0(svelte@5.45.8) '@milkdown-lab/plugin-split-editing': specifier: ^1.3.1 - version: 1.3.1(@milkdown/core@7.17.1)(@milkdown/prose@7.17.1) + version: 1.3.1(@milkdown/core@7.17.3)(@milkdown/prose@7.17.3) '@milkdown/core': specifier: ^7.17.1 - version: 7.17.1 + version: 7.17.3 '@milkdown/ctx': specifier: ^7.17.1 - version: 7.17.1 + version: 7.17.3 '@milkdown/plugin-history': specifier: ^7.17.1 - version: 7.17.1 + version: 7.17.3 '@milkdown/plugin-listener': specifier: ^7.17.1 - version: 7.17.1 + version: 7.17.3 '@milkdown/preset-commonmark': specifier: ^7.17.1 - version: 7.17.1 + version: 7.17.3 '@milkdown/preset-gfm': specifier: ^7.17.1 - version: 7.17.1 + version: 7.17.3 '@milkdown/theme-nord': specifier: ^7.17.1 - version: 7.17.1 + version: 7.17.3 '@milkdown/utils': specifier: ^7.17.1 - version: 7.17.1 - '@modelcontextprotocol/sdk': - specifier: ^1.22.0 - version: 1.22.0 + version: 7.17.3 '@node-rs/argon2': specifier: ^2.0.2 version: 2.0.2 '@prosemark/core': specifier: ^0.0.4 - version: 0.0.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.0)(@codemirror/lang-markdown@6.5.0)(@codemirror/language-data@6.5.2)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3) + version: 0.0.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.0)(@codemirror/lang-markdown@6.5.0)(@codemirror/language-data@6.5.2)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.39.0)(@lezer/highlight@1.2.3) '@prosemark/paste-rich-text': specifier: ^0.0.2 version: 0.0.2 @@ -103,16 +106,16 @@ importers: version: 0.0.5(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.0)(@codemirror/lang-markdown@6.5.0)(@codemirror/language-data@6.5.2)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@lezer/highlight@1.2.3) '@tiptap/core': specifier: ^3.11.0 - version: 3.11.0(@tiptap/pm@3.11.0) + version: 3.13.0(@tiptap/pm@3.13.0) '@tiptap/extension-mathematics': specifier: ^3.11.0 - version: 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)(katex@0.16.25) + version: 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)(katex@0.16.27) '@tiptap/extension-typography': specifier: ^3.11.0 - version: 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + version: 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) '@tiptap/starter-kit': specifier: ^3.11.0 - version: 3.11.0 + version: 3.13.0 bcryptjs: specifier: ^3.0.3 version: 3.0.3 @@ -130,25 +133,31 @@ importers: version: 17.2.3 effect: specifier: ^3.19.6 - version: 3.19.6 - fast-diff: - specifier: ^1.3.0 - version: 1.3.0 + version: 3.19.9 katex: specifier: ^0.16.25 - version: 0.16.25 + version: 0.16.27 + loro-codemirror: + specifier: ^0.3.3 + version: 0.3.3(@codemirror/state@6.5.2)(@codemirror/view@6.39.0)(loro-crdt@1.10.3) loro-crdt: specifier: ^1.10.0 - version: 1.10.0 + version: 1.10.3 + runed: + specifier: ^0.37.0 + version: 0.37.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(zod@4.1.13) svelte: - specifier: ^5.44.0 - version: 5.44.0 + specifier: ^5.45.8 + version: 5.45.8 + temporal-polyfill: + specifier: ^0.3.0 + version: 0.3.0 tiptap-markdown: specifier: ^0.9.0 - version: 0.9.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) + version: 0.9.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) typescript-svelte-plugin: specifier: ^0.3.50 - version: 0.3.50(svelte@5.44.0)(typescript@5.9.3) + version: 0.3.50(svelte@5.45.8)(typescript@5.9.3) devDependencies: '@effect/language-service': specifier: ^0.56.0 @@ -170,25 +179,25 @@ importers: version: 1.1.0 '@sveltejs/adapter-auto': specifier: ^7.0.0 - version: 7.0.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1))) + version: 7.0.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))) '@sveltejs/kit': - specifier: ^2.49.0 - version: 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + specifier: ^2.49.2 + version: 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) '@sveltejs/vite-plugin-svelte': specifier: ^6.2.1 - version: 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + version: 6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) '@tailwindcss/typography': specifier: ^0.5.19 version: 0.5.19(tailwindcss@4.1.17) '@tailwindcss/vite': specifier: ^4.1.17 - version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + version: 4.1.17(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) '@types/node': specifier: ^24.10.1 version: 24.10.1 drizzle-kit: specifier: ^0.31.7 - version: 0.31.7 + version: 0.31.8 drizzle-orm: specifier: ^0.44.7 version: 0.44.7(@libsql/client@0.15.15) @@ -197,25 +206,28 @@ importers: version: 9.39.1(jiti@2.6.1) eslint-plugin-svelte: specifier: ^3.13.0 - version: 3.13.0(eslint@9.39.1(jiti@2.6.1))(svelte@5.44.0) + version: 3.13.1(eslint@9.39.1(jiti@2.6.1))(svelte@5.45.8) globals: specifier: ^16.5.0 version: 16.5.0 + jiti: + specifier: ^2.6.1 + version: 2.6.1 playwright: specifier: ^1.56.1 - version: 1.56.1 + version: 1.57.0 prettier: specifier: ^3.6.2 - version: 3.6.2 + version: 3.7.4 prettier-plugin-svelte: specifier: ^3.4.0 - version: 3.4.0(prettier@3.6.2)(svelte@5.44.0) + version: 3.4.0(prettier@3.7.4)(svelte@5.45.8) prettier-plugin-tailwindcss: specifier: ^0.7.1 - version: 0.7.1(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0))(prettier@3.6.2) + version: 0.7.2(prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.45.8))(prettier@3.7.4) svelte-check: specifier: ^4.3.4 - version: 4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3) + version: 4.3.4(picomatch@4.0.3)(svelte@5.45.8)(typescript@5.9.3) tailwindcss: specifier: ^4.1.17 version: 4.1.17 @@ -224,19 +236,19 @@ importers: version: 5.9.3 typescript-eslint: specifier: ^8.47.0 - version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + version: 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) vite: specifier: ^7.2.4 - version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + version: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) vite-plugin-devtools-json: specifier: ^1.0.0 - version: 1.0.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + version: 1.0.0(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) vite-plugin-top-level-await: specifier: ^1.6.0 - version: 1.6.0(rollup@4.53.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + version: 1.6.0(rollup@4.53.3)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) vite-plugin-wasm: specifier: ^3.5.0 - version: 3.5.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + version: 3.5.0(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) packages: @@ -353,8 +365,8 @@ packages: '@codemirror/theme-one-dark@6.1.3': resolution: {integrity: sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==} - '@codemirror/view@6.38.8': - resolution: {integrity: sha512-XcE9fcnkHCbWkjeKyi0lllwXmBLtyYb5dt89dJyx23I9+LSh5vZDIuk7OLG4VM1lgrXZQcY6cxyZyk5WVPRv/A==} + '@codemirror/view@6.39.0': + resolution: {integrity: sha512-pn7UA5RDNLFpdM4PTyqwb1qQ/hQ3brwUKYAlJGrg3972VHJotgXrVBdBAWcbMkOjERXX609fmqfRldnGkC96kw==} '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} @@ -363,10 +375,10 @@ packages: resolution: {integrity: sha512-gvJaHoeXMHAoA6+Xyj9Vdq52yDCs+ECLbKpHvxHtdJP/C0D9b3JFEfLjdVuw37zoWcYS856um4rgEYHlW2LSEQ==} hasBin: true - '@effect/platform@0.93.3': - resolution: {integrity: sha512-s88zctkeXba24Mjy7MEFMuam1p5sXmsG7uQjPIDE6EiC+2IFUQd8976TtangiU0e8qu0SALpjIH1P1QyC7/1og==} + '@effect/platform@0.93.6': + resolution: {integrity: sha512-I5lBGQWzWXP4zlIdPs7z7WHmEFVBQhn+74emr/h16GZX96EEJ6I1rjGaKyZF7mtukbMuo9wEckDPssM8vskZ/w==} peerDependencies: - effect: ^3.19.4 + effect: ^3.19.8 '@emnapi/core@1.7.1': resolution: {integrity: sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==} @@ -704,8 +716,8 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.1': - resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==} + '@eslint/eslintrc@3.3.3': + resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.39.1': @@ -720,6 +732,18 @@ packages: resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@hpke/chacha20poly1305@1.7.1': + resolution: {integrity: sha512-Zp8IwRIkdCucu877wCNqDp3B8yOhAnAah/YDDkO94pPr/KKV7IGnBbpwIjDB3BsAySWBMrhhdE0JKYw3N4FCag==} + engines: {node: '>=16.0.0'} + + '@hpke/common@1.8.1': + resolution: {integrity: sha512-PSI4QSxH8XDli0TqAsWycVfrLLCM/bBe+hVlJwtuJJiKIvCaFS3CXX/WtRfJceLJye9NHc2J7GvHVCY9B1BEbA==} + engines: {node: '>=16.0.0'} + + '@hpke/core@1.7.5': + resolution: {integrity: sha512-4xfckZuPaIodeu0HpuTRIdtmajhRHXM/6rjS2N62Ns9aOCkGbbeYRwktqR3bUScuhCwyEBsEQqtIh9f0iLP3WQ==} + engines: {node: '>=16.0.0'} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -752,8 +776,8 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@lezer/common@1.3.0': - resolution: {integrity: sha512-L9X8uHCYU310o99L3/MpJKYxPzXPOS7S0NmBaM7UO/x2Kb2WbmMLSkfvdr1KxRIFYOpbY0Jhn7CfLSUDzL8arQ==} + '@lezer/common@1.4.0': + resolution: {integrity: sha512-DVeMRoGrgn/k45oQNu189BoW4SZwgZFzJ1+1TV5j2NJ/KFC83oa/enRqZSGshyeMk5cPWMhsKs9nx+8o0unwGg==} '@lezer/cpp@1.1.3': resolution: {integrity: sha512-ykYvuFQKGsRi6IcE+/hCSGUhb/I4WPjd3ELhEblm2wS2cOznDFzO+ubK2c+ioysOnlZ3EduV+MVQFCPzAIoY3w==} @@ -779,11 +803,11 @@ packages: '@lezer/json@1.0.3': resolution: {integrity: sha512-BP9KzdF9Y35PDpv04r0VeSTKDeox5vVr3efE7eBbx3r4s3oNLfunchejZhjArmeieBH+nVOpgIiBJpEAv8ilqQ==} - '@lezer/lr@1.4.3': - resolution: {integrity: sha512-yenN5SqAxAPv/qMnpWW0AT7l+SxVrgG+u0tNsRQWqbrz66HIl8DnEbBObvy21J5K7+I1v7gsAnlE2VQ5yYVSeA==} + '@lezer/lr@1.4.4': + resolution: {integrity: sha512-LHL17Mq0OcFXm1pGQssuGTQFPPdxARjKM8f7GA5+sGtHi0K3R84YaSbmche0+RKWHnCsx9asEe5OWOI4FHfe4A==} - '@lezer/markdown@1.6.0': - resolution: {integrity: sha512-AXb98u3M6BEzTnreBnGtQaF7xFTiMA92Dsy5tqEjpacbjRxDSFdN4bKJo9uvU4cEEOS7D2B9MT7kvDgOEIzJSw==} + '@lezer/markdown@1.6.1': + resolution: {integrity: sha512-72ah+Sml7lD8Wn7lnz9vwYmZBo9aQT+I2gjK/0epI+gjdwUbWw3MJ/ZBGEqG1UfrIauRqH37/c5mVHXeCTGXtA==} '@lezer/php@1.0.5': resolution: {integrity: sha512-W7asp9DhM6q0W6DYNwIkLSKOvxlXRrif+UXBMxzsJUuqmhE7oVU+gS3THO4S/Puh7Xzgm858UNaFi6dxTP8dJA==} @@ -878,51 +902,42 @@ packages: '@milkdown/core': '>=7.0.0' '@milkdown/prose': '>=7.0.0' - '@milkdown/core@7.17.1': - resolution: {integrity: sha512-JzmBd3YBxsrZUnXG4AM3jtOCyDhg8nMUeBwtVoS6h3qF1pduZZzXnkpjyAsDc2ZDGjnhlTOS9mNcyQUfDj1PXQ==} + '@milkdown/core@7.17.3': + resolution: {integrity: sha512-jaXERBsH+I8wtJ20nHs30zoAQ8O3BWmO1DzuiCx3tmq6VB1M/xQQmU0yTiZBkL5SlC52AH+6YQgpC7L40BT02A==} - '@milkdown/ctx@7.17.1': - resolution: {integrity: sha512-TPPo54lkSUFtWtd9L9nujQznsguJ1QEHGbt09Y7KcGVJC92jIHyeAgxRwRBR3EYoe0HhSJI3eaxxzI/VtcODEg==} + '@milkdown/ctx@7.17.3': + resolution: {integrity: sha512-fIj/GkJhr218VtIbAvhEgakZD2joGXCfAh9Ay1XwM2DR2RnjxtnJuywjdUnhyswupTmYmp/k0mUHSFUa8qI9yQ==} - '@milkdown/exception@7.17.1': - resolution: {integrity: sha512-JbIWyfQXKuDfLGTQDXM9asYTtiLF2qMf9oRGtMb0PuQrs7I1Z2zhwx/ee112ZZMxHtCXWDYnFrTi8LXhE1ZqGg==} + '@milkdown/exception@7.17.3': + resolution: {integrity: sha512-LDtyId/VZdOcVsctRg7gBGtq1C1zxBghKlOTdB50Uc36Hp2QAqc2IjD4cv3TwQblneUxs0X16MXnxCA9Q8bw2g==} - '@milkdown/plugin-history@7.17.1': - resolution: {integrity: sha512-yyqYxnvbE19c1qWtKFX3llvsjZ06AAvJU5Eb+RNnZFuqpfXyPkgRwh2T1Wo+WnlFVCjiotRee46y8BLQlLCqdQ==} + '@milkdown/plugin-history@7.17.3': + resolution: {integrity: sha512-mAlEqWF45lCr/xqZ/ZhFovI+DHi4kr+WIY8kLP5v6VXRwrt4OWJapzXVSJ37sN+n+DSfVRxlyCKPCDRHX5B9kQ==} - '@milkdown/plugin-listener@7.17.1': - resolution: {integrity: sha512-v0ga+efth4D2Cr+ffCvM7BHSoqOQX+ySy/gzRH2yiBH0dLnLHgJP5V5O6U4Z4r8UH/Wx0BgZ0FIRngUEhh/Aqw==} + '@milkdown/plugin-listener@7.17.3': + resolution: {integrity: sha512-plThVsl9JDopMfienFxlEpzq/k9sAqaG3VF9kknVR/ELLsJ6Qq07n1wXBHiC4P9TZqzLzR+WNOmeMNj0lNI4jg==} - '@milkdown/preset-commonmark@7.17.1': - resolution: {integrity: sha512-kWiH91tJ1qdzGM3F7Yr6HmsnyLLbwQqpYo2P6QJCkOzWhABOwTx5jQuGwdgKA7qt+wk0TK7AisXSCcbHiwUwfg==} + '@milkdown/preset-commonmark@7.17.3': + resolution: {integrity: sha512-f875wPTwg5kxKYuDWs0S9AFVcaY0PjQ1YpO0NsAdVgDANwurUpqUY9/KoRKJGAFDRiE59Yq5YD9Lx5vnQ07OuQ==} - '@milkdown/preset-gfm@7.17.1': - resolution: {integrity: sha512-clHa77NLTbf9FQBbKa1ucMFDUojnIbqeg9edIS9XL1LrByGZXaaLZUvRhCktFHwg9qloF778vX4McW3tLHVgfQ==} + '@milkdown/preset-gfm@7.17.3': + resolution: {integrity: sha512-IY+Hyhe6cCUF2Fi+KNfzbFdTaepKCkYNbbv65Dze7Dkb0VgoPfyiRdzznKiX7luXJnZv8Y+QHr+8KXirxzKVfg==} - '@milkdown/prose@7.17.1': - resolution: {integrity: sha512-0Kr42QUR1mAh45R7AQPqct5MeGCRWvV4qfQN92a8DpLYIsvi9XpEGAB7m/KUUxYTBCrRN2ZHpcd+p9vhP0tldA==} + '@milkdown/prose@7.17.3': + resolution: {integrity: sha512-ZgHUJGKWwIo52TrBzeStz7wsrKBineJ1b1FJMtsJjC9RElXaYzZZEmT7vP1aItknX8UXDr1m0a4fWKcCtykcjw==} - '@milkdown/theme-nord@7.17.1': - resolution: {integrity: sha512-axiJjpBIi54z6EFZS/zvmp9Xeo0W2ZaO5jyyAC62yUOsm5clyTaq4vep6v0CDLoOyAtzIgCwRdKyO+C6tbpHeA==} + '@milkdown/theme-nord@7.17.3': + resolution: {integrity: sha512-gdCLQQ6N0NB1BLasjFGzf4jxbokA5DuI2Nl33kinvtJmzQQMuTx2fZqhztollLQnlMf9odGqX6gu5rD6xWRjkg==} - '@milkdown/transformer@7.17.1': - resolution: {integrity: sha512-RfKo+v1R0UWlLgpbKSFSTtKc3Hd7RAd0vRLR/d5ku4/5IpOGYplGEUsxF77qVgik6/yiox79R3HqkQNJ5e4/3Q==} + '@milkdown/transformer@7.17.3': + resolution: {integrity: sha512-XIeMV/X6R9YhaYSTytZGOwVMsVCXBgXVaP9OpABMceXR9hYfbrZZlurXirTMJYayYZ3IVZZJHQ56/wQ4URD2pg==} - '@milkdown/utils@7.17.1': - resolution: {integrity: sha512-QTjbaxv+ZOB4a1BaQULkeJExyIvMnQw69UKf9QoM/E8iY2q1c8kppnN6i6ZeN9ZkCh4lXu+r7w/LH6zSFXrsdA==} + '@milkdown/utils@7.17.3': + resolution: {integrity: sha512-8JWqfhdupzoZMv7btqYAAXRyqhaOGffh2zpSoD+t9enL0MFvIHsenWS8W8h7XRTL6E6+N6kFZ1qyBHMg6E5Idg==} '@mixmark-io/domino@2.2.0': resolution: {integrity: sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==} - '@modelcontextprotocol/sdk@1.22.0': - resolution: {integrity: sha512-VUpl106XVTCpDmTBil2ehgJZjhyLY2QZikzF8NvTXtLRF1CvO5iEE2UNZdVIUer35vFOwMKYeUGbjJtvPWan3g==} - engines: {node: '>=18'} - peerDependencies: - '@cfworker/json-schema': ^4.1.1 - peerDependenciesMeta: - '@cfworker/json-schema': - optional: true - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} cpu: [arm64] @@ -1046,18 +1061,6 @@ packages: resolution: {integrity: sha512-t64wIsPEtNd4aUPuTAyeL2ubxATCBGmeluaKXEMAFk/8w6AJIVVkeLKMBpgLW6LU2t5cQxT+env/c6jxbtTQBg==} engines: {node: '>= 10'} - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} - - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} - - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} - '@oslojs/asn1@1.0.0': resolution: {integrity: sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA==} @@ -1222,8 +1225,8 @@ packages: '@standard-schema/spec@1.0.0': resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==} - '@sveltejs/acorn-typescript@1.0.7': - resolution: {integrity: sha512-znp1A/Y1Jj4l/Zy7PX5DZKBE0ZNY+5QBngiE21NJkfSTyzzC5iKNWOtwFXKtIrn7MXEFBck4jD95iBNkGjK92Q==} + '@sveltejs/acorn-typescript@1.0.8': + resolution: {integrity: sha512-esgN+54+q0NjB0Y/4BomT9samII7jGwNy/2a3wNZbT2A2RpmXsXwUt24LvLhx6jUq2gVk4cWEvcRO6MFQbOfNA==} peerDependencies: acorn: ^8.9.0 @@ -1232,8 +1235,8 @@ packages: peerDependencies: '@sveltejs/kit': ^2.0.0 - '@sveltejs/kit@2.49.0': - resolution: {integrity: sha512-oH8tXw7EZnie8FdOWYrF7Yn4IKrqTFHhXvl8YxXxbKwTMcD/5NNCryUSEXRk2ZR4ojnub0P8rNrsVGHXWqIDtA==} + '@sveltejs/kit@2.49.2': + resolution: {integrity: sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==} engines: {node: '>=18.13'} hasBin: true peerDependencies: @@ -1433,143 +1436,143 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 - '@tiptap/core@3.11.0': - resolution: {integrity: sha512-kmS7ZVpHm1EMnW1Wmft9H5ZLM7E0G0NGBx+aGEHGDcNxZBXD2ZUa76CuWjIhOGpwsPbELp684ZdpF2JWoNi4Dg==} + '@tiptap/core@3.13.0': + resolution: {integrity: sha512-iUelgiTMgPVMpY5ZqASUpk8mC8HuR9FWKaDzK27w9oWip9tuB54Z8mePTxNcQaSPb6ErzEaC8x8egrRt7OsdGQ==} peerDependencies: - '@tiptap/pm': ^3.11.0 + '@tiptap/pm': ^3.13.0 - '@tiptap/extension-blockquote@3.11.0': - resolution: {integrity: sha512-0H8WVW6Vn4GJ7sQ6wfyDgUU+DqM8fp62g8N0fFPiEhoYtpIYUmCqGhpKnqYR0tet6ofFa648XmA6n2VX7sugzw==} + '@tiptap/extension-blockquote@3.13.0': + resolution: {integrity: sha512-K1z/PAIIwEmiWbzrP//4cC7iG1TZknDlF1yb42G7qkx2S2X4P0NiqX7sKOej3yqrPjKjGwPujLMSuDnCF87QkQ==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-bold@3.11.0': - resolution: {integrity: sha512-V/c3XYO09Le9GlBGq1MK4c97Fffi0GADQTbZ+LFoi65nUrAwutn5wYnXBcEyWQI6RmFWVDJTieamqtc4j9teyw==} + '@tiptap/extension-bold@3.13.0': + resolution: {integrity: sha512-VYiDN9EEwR6ShaDLclG8mphkb/wlIzqfk7hxaKboq1G+NSDj8PcaSI9hldKKtTCLeaSNu6UR5nkdu/YHdzYWTw==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-bullet-list@3.11.0': - resolution: {integrity: sha512-IKdb1C3bHA1sGPiUcntkL+wHebRg71K5+tgaaRnMw0qmtcpcOQb5zhQOSm5bXUsgCk/WgT04dkZPnpn6Gg1PvQ==} + '@tiptap/extension-bullet-list@3.13.0': + resolution: {integrity: sha512-fFQmmEUoPzRGiQJ/KKutG35ZX21GE+1UCDo8Q6PoWH7Al9lex47nvyeU1BiDYOhcTKgIaJRtEH5lInsOsRJcSA==} peerDependencies: - '@tiptap/extension-list': ^3.11.0 + '@tiptap/extension-list': ^3.13.0 - '@tiptap/extension-code-block@3.11.0': - resolution: {integrity: sha512-y01RJVbygDJWYXxZ0SiCYwvUF2X91RANCLSdb8X0qiwVPgNOzsDrrzS/iqoXkiYmM93pJw+ZWelEZxRvxEwsrg==} + '@tiptap/extension-code-block@3.13.0': + resolution: {integrity: sha512-kIwfQ4iqootsWg9e74iYJK54/YMIj6ahUxEltjZRML5z/h4gTDcQt2eTpnEC8yjDjHeUVOR94zH9auCySyk9CQ==} peerDependencies: - '@tiptap/core': ^3.11.0 - '@tiptap/pm': ^3.11.0 + '@tiptap/core': ^3.13.0 + '@tiptap/pm': ^3.13.0 - '@tiptap/extension-code@3.11.0': - resolution: {integrity: sha512-5OpR5O4bveHe1KG9CJsto86NgkuerYq3OLY78vzh9uFCLdv7xgXA2aZYJfRMhbZ7hKsR7hHg1etBJUCk+TKsMg==} + '@tiptap/extension-code@3.13.0': + resolution: {integrity: sha512-sF5raBni6iSVpXWvwJCAcOXw5/kZ+djDHx1YSGWhopm4+fsj0xW7GvVO+VTwiFjZGKSw+K5NeAxzcQTJZd3Vhw==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-document@3.11.0': - resolution: {integrity: sha512-N2G3cwL2Dtur/CgD/byJmFx9T5no6fTO/U462VP3rthQYrRA1AB3TCYqtlwJkmyoxRTNd4qIg4imaPl8ej6Heg==} + '@tiptap/extension-document@3.13.0': + resolution: {integrity: sha512-RjU7hTJwjKXIdY57o/Pc+Yr8swLkrwT7PBQ/m+LCX5oO/V2wYoWCjoBYnK5KSHrWlNy/aLzC33BvLeqZZ9nzlQ==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-dropcursor@3.11.0': - resolution: {integrity: sha512-gW/QMGAyiXGSpO+X/lTeiBQn1Or8T8UVB3y9Cv2Lh6zx0SWU+FA28EH+y6s3fm872reN4dH/9rEvMuJjhU/BEw==} + '@tiptap/extension-dropcursor@3.13.0': + resolution: {integrity: sha512-m7GPT3c/83ni+bbU8c+3dpNa8ug+aQ4phNB1Q52VQG3oTonDJnZS7WCtn3lB/Hi1LqoqMtEHwhepU2eD+JeXqQ==} peerDependencies: - '@tiptap/extensions': ^3.11.0 + '@tiptap/extensions': ^3.13.0 - '@tiptap/extension-gapcursor@3.11.0': - resolution: {integrity: sha512-lXGEZiYX7k/pEFr8BgDE91vqjLTwuf+qhHLTgIpfhbt562nShLPIDj9Vzu3xrR4fwUAMiUNiLyaeInb8j3I4kg==} + '@tiptap/extension-gapcursor@3.13.0': + resolution: {integrity: sha512-KVxjQKkd964nin+1IdM2Dvej/Jy4JTMcMgq5seusUhJ9T9P8F9s2D5Iefwgkps3OCzub/aF+eAsZe+1P5KSIgA==} peerDependencies: - '@tiptap/extensions': ^3.11.0 + '@tiptap/extensions': ^3.13.0 - '@tiptap/extension-hard-break@3.11.0': - resolution: {integrity: sha512-NJEHTj++kFOayQXKSQSi9j9eAG33eSiJqai2pf4U+snW94fmb8cYLUurDmfYRe20O6EzBSX0X3GjVlkOz+5b7A==} + '@tiptap/extension-hard-break@3.13.0': + resolution: {integrity: sha512-nH1OBaO+/pakhu+P1jF208mPgB70IKlrR/9d46RMYoYbqJTNf4KVLx5lHAOHytIhjcNg+MjyTfJWfkK+dyCCyg==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-heading@3.11.0': - resolution: {integrity: sha512-4Eo67Yo7vsYLkizcMoGdZAR9aHbC7FFTrqfNEd4Em3ajRi0iNqyWMaI90UCYlitDdRdqFlq/njWrMqBOLUgaWQ==} + '@tiptap/extension-heading@3.13.0': + resolution: {integrity: sha512-8VKWX8waYPtUWN97J89em9fOtxNteh6pvUEd0htcOAtoxjt2uZjbW5N4lKyWhNKifZBrVhH2Cc2NUPuftCVgxw==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-horizontal-rule@3.11.0': - resolution: {integrity: sha512-FugFHZG+oiMBV6k42hn9NOA4wRNc2b9UeEIMR+XwEMpWJInV4VwSwDvu8JClgkDo8z7FEnker9e51DZ00CLWqg==} + '@tiptap/extension-horizontal-rule@3.13.0': + resolution: {integrity: sha512-ZUFyORtjj22ib8ykbxRhWFQOTZjNKqOsMQjaAGof30cuD2DN5J5pMz7Haj2fFRtLpugWYH+f0Mi+WumQXC3hCw==} peerDependencies: - '@tiptap/core': ^3.11.0 - '@tiptap/pm': ^3.11.0 + '@tiptap/core': ^3.13.0 + '@tiptap/pm': ^3.13.0 - '@tiptap/extension-italic@3.11.0': - resolution: {integrity: sha512-WP6wL2b//8bLVdeUCWOpYA7nUStvrAMMD0nRn0F9CEW+l7vH6El2PZFhHmJ9uqXo5MnyugBpARiwgxfoAlef5w==} + '@tiptap/extension-italic@3.13.0': + resolution: {integrity: sha512-XbVTgmzk1kgUMTirA6AGdLTcKHUvEJoh3R4qMdPtwwygEOe7sBuvKuLtF6AwUtpnOM+Y3tfWUTNEDWv9AcEdww==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-link@3.11.0': - resolution: {integrity: sha512-RoUkGqowVMKLE76KktNOGhzNMyKtwrSDRqeYCe1ODPuOMZvDGexOE8cIuA4A1ODkgN6ji9qE/9Sf8uhpZdH39Q==} + '@tiptap/extension-link@3.13.0': + resolution: {integrity: sha512-LuFPJ5GoL12GHW4A+USsj60O90pLcwUPdvEUSWewl9USyG6gnLnY/j5ZOXPYH7LiwYW8+lhq7ABwrDF2PKyBbA==} peerDependencies: - '@tiptap/core': ^3.11.0 - '@tiptap/pm': ^3.11.0 + '@tiptap/core': ^3.13.0 + '@tiptap/pm': ^3.13.0 - '@tiptap/extension-list-item@3.11.0': - resolution: {integrity: sha512-KXTTSBH/T/WW8O1YhK/lVmwlSGh2w2VVucUkMLhgk1VPchahAkn2LfgbgKrCRG/F8M8Jlfvz67iJDo6+bbNqew==} + '@tiptap/extension-list-item@3.13.0': + resolution: {integrity: sha512-63NbcS/XeQP2jcdDEnEAE3rjJICDj8y1SN1h/MsJmSt1LusnEo8WQ2ub86QELO6XnD3M04V03cY6Knf6I5mTkw==} peerDependencies: - '@tiptap/extension-list': ^3.11.0 + '@tiptap/extension-list': ^3.13.0 - '@tiptap/extension-list-keymap@3.11.0': - resolution: {integrity: sha512-vm1zGdEqcbQnrGlVXchk1ibmTsyxyfGcGPVWsc4MG+UAFcNfcpAnvCar71BF4RGGPtpzOWdqGkvJENyh0L5/Hw==} + '@tiptap/extension-list-keymap@3.13.0': + resolution: {integrity: sha512-P+HtIa1iwosb1feFc8B/9MN5EAwzS+/dZ0UH0CTF2E4wnp5Z9OMxKl1IYjfiCwHzZrU5Let+S/maOvJR/EmV0g==} peerDependencies: - '@tiptap/extension-list': ^3.11.0 + '@tiptap/extension-list': ^3.13.0 - '@tiptap/extension-list@3.11.0': - resolution: {integrity: sha512-4Ane7VCVZ+GFOQNuy2nMP+SoWH7EemC3geTTqvgHm1H0tbSosxLJAVaZ9dF06F35RJmYCm+jLJUhRVd156eCRQ==} + '@tiptap/extension-list@3.13.0': + resolution: {integrity: sha512-MMFH0jQ4LeCPkJJFyZ77kt6eM/vcKujvTbMzW1xSHCIEA6s4lEcx9QdZMPpfmnOvTzeoVKR4nsu2t2qT9ZXzAw==} peerDependencies: - '@tiptap/core': ^3.11.0 - '@tiptap/pm': ^3.11.0 + '@tiptap/core': ^3.13.0 + '@tiptap/pm': ^3.13.0 - '@tiptap/extension-mathematics@3.11.0': - resolution: {integrity: sha512-Zen7O1Y/LuzPLQEpMlY9GkW0hWI8qL9RcpL1M+vW935JPM3dPFdrAs5U/iSu4CDHOsypuORyd9CM50RROPNEvw==} + '@tiptap/extension-mathematics@3.13.0': + resolution: {integrity: sha512-26tJc+L0pkQhKcMZKlpUtDx/B8LSM5YitaxYbtsxN/fTLzznq+NkwK6VgxlNyseHEcpTxDzOw4jTkV0CDWo4mQ==} peerDependencies: - '@tiptap/core': ^3.11.0 - '@tiptap/pm': ^3.11.0 + '@tiptap/core': ^3.13.0 + '@tiptap/pm': ^3.13.0 katex: ^0.16.4 - '@tiptap/extension-ordered-list@3.11.0': - resolution: {integrity: sha512-kO8GH4w4Xil+qPiHJLAyILdGHF9hCjkhoVtPD8YEfqK6Qx3bZql5FPySCQNs+MU6rLSCCdam8SUPGY/+SCufqA==} + '@tiptap/extension-ordered-list@3.13.0': + resolution: {integrity: sha512-QuDyLzuK/3vCvx9GeKhgvHWrGECBzmJyAx6gli2HY+Iil7XicbfltV4nvhIxgxzpx3LDHLKzJN9pBi+2MzX60g==} peerDependencies: - '@tiptap/extension-list': ^3.11.0 + '@tiptap/extension-list': ^3.13.0 - '@tiptap/extension-paragraph@3.11.0': - resolution: {integrity: sha512-hxgjZOXOqstRTWv+QjWJjK23rD5qzIV9ePlhX3imLeq/MgX0aU9VBDaG5SGKbSjaBNQnpLw6+sABJi3CDP6Z5A==} + '@tiptap/extension-paragraph@3.13.0': + resolution: {integrity: sha512-9csQde1i0yeZI5oQQ9e1GYNtGL2JcC2d8Fwtw9FsGC8yz2W0h+Fmk+3bc2kobbtO5LGqupSc1fKM8fAg5rSRDg==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-strike@3.11.0': - resolution: {integrity: sha512-XVP/WMYLrqLBfUsGPu2H9MrOUZLhGUaxtZ3hSRffDi/lsw53x/coZ9eO0FxOB9R7z2ksHWmticIs+0YnKt9LNQ==} + '@tiptap/extension-strike@3.13.0': + resolution: {integrity: sha512-VHhWNqTAMOfrC48m2FcPIZB0nhl6XHQviAV16SBc+EFznKNv9tQUsqQrnuQ2y6ZVfqq5UxvZ3hKF/JlN/Ff7xw==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-text@3.11.0': - resolution: {integrity: sha512-ELAYm2BuChzZOqDG9B0k3W6zqM4pwNvXkam28KgHGiT2y7Ni68Rb+NXp16uVR+5zR6hkqnQ/BmJSKzAW59MXpA==} + '@tiptap/extension-text@3.13.0': + resolution: {integrity: sha512-VcZIna93rixw7hRkHGCxDbL3kvJWi80vIT25a2pXg0WP1e7Pi3nBYvZIL4SQtkbBCji9EHrbZx3p8nNPzfazYw==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-typography@3.11.0': - resolution: {integrity: sha512-a9WcHxfeuWlGw58oOXHJk8sI0xTLSoPraZCztK77Pn69ry2GiNz0pFoL7WakThaqzFnlXtwDcl0TQTV9aqDjQA==} + '@tiptap/extension-typography@3.13.0': + resolution: {integrity: sha512-Pvxc0Mu3fIgcqOVpU5DqK55F+/ShvX020HmbsPY+Z7SED9fkan5QVn3n2nm13A2TQ+RWDlPAnHe7Gh0d/KsL5Q==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extension-underline@3.11.0': - resolution: {integrity: sha512-D3PsS/84RlQKFjd5eerMIUioC0mNh4yy1RRV/WbXx6ugu+6T+0hT42gNk9Ap8pDsVQZCk0SHfDyBEUFC2KOwKw==} + '@tiptap/extension-underline@3.13.0': + resolution: {integrity: sha512-VDQi+UYw0tFnfghpthJTFmtJ3yx90kXeDwFvhmT8G+O+si5VmP05xYDBYBmYCix5jqKigJxEASiBL0gYOgMDEg==} peerDependencies: - '@tiptap/core': ^3.11.0 + '@tiptap/core': ^3.13.0 - '@tiptap/extensions@3.11.0': - resolution: {integrity: sha512-g43beA73ZMLezez1st9LEwYrRHZ0FLzlsSlOZKk7sdmtHLmuqWHf4oyb0XAHol1HZIdGv104rYaGNgmQXr1ecQ==} + '@tiptap/extensions@3.13.0': + resolution: {integrity: sha512-i7O0ptSibEtTy+2PIPsNKEvhTvMaFJg1W4Oxfnbuxvaigs7cJV9Q0lwDUcc7CPsNw2T1+44wcxg431CzTvdYoA==} peerDependencies: - '@tiptap/core': ^3.11.0 - '@tiptap/pm': ^3.11.0 + '@tiptap/core': ^3.13.0 + '@tiptap/pm': ^3.13.0 - '@tiptap/pm@3.11.0': - resolution: {integrity: sha512-plCQDLCZIOc92cizB8NNhBRN0szvYR3cx9i5IXo6v9Xsgcun8KHNcJkesc2AyeqdIs0BtOJZaqQ9adHThz8UDw==} + '@tiptap/pm@3.13.0': + resolution: {integrity: sha512-WKR4ucALq+lwx0WJZW17CspeTpXorbIOpvKv5mulZica6QxqfMhn8n1IXCkDws/mCoLRx4Drk5d377tIjFNsvQ==} - '@tiptap/starter-kit@3.11.0': - resolution: {integrity: sha512-8kMMYqVSZ2Oqji+mY1o9meTjCRWp4DplFegu7APqDEQRhlb6mBI0wNuazYb7FKJIHJTtf0F6cYglJrxpu9c/fA==} + '@tiptap/starter-kit@3.13.0': + resolution: {integrity: sha512-Ojn6sRub04CRuyQ+9wqN62JUOMv+rG1vXhc2s6DCBCpu28lkCMMW+vTe7kXJcEdbot82+5swPbERw9vohswFzg==} '@tybys/wasm-util@0.10.1': resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} @@ -1631,69 +1634,65 @@ packages: '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} - '@typescript-eslint/eslint-plugin@8.47.0': - resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==} + '@typescript-eslint/eslint-plugin@8.49.0': + resolution: {integrity: sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.47.0 + '@typescript-eslint/parser': ^8.49.0 eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/parser@8.47.0': - resolution: {integrity: sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==} + '@typescript-eslint/parser@8.49.0': + resolution: {integrity: sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.47.0': - resolution: {integrity: sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==} + '@typescript-eslint/project-service@8.49.0': + resolution: {integrity: sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/scope-manager@8.47.0': - resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==} + '@typescript-eslint/scope-manager@8.49.0': + resolution: {integrity: sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.47.0': - resolution: {integrity: sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==} + '@typescript-eslint/tsconfig-utils@8.49.0': + resolution: {integrity: sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/type-utils@8.47.0': - resolution: {integrity: sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==} + '@typescript-eslint/type-utils@8.49.0': + resolution: {integrity: sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/types@8.47.0': - resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==} + '@typescript-eslint/types@8.49.0': + resolution: {integrity: sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.47.0': - resolution: {integrity: sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==} + '@typescript-eslint/typescript-estree@8.49.0': + resolution: {integrity: sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.47.0': - resolution: {integrity: sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==} + '@typescript-eslint/utils@8.49.0': + resolution: {integrity: sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/visitor-keys@8.47.0': - resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==} + '@typescript-eslint/visitor-keys@8.49.0': + resolution: {integrity: sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - accepts@2.0.0: - resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} - engines: {node: '>= 0.6'} - acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1704,14 +1703,6 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ajv-formats@3.0.1: - resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} - peerDependencies: - ajv: ^8.0.0 - peerDependenciesMeta: - ajv: - optional: true - ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1746,35 +1737,15 @@ packages: bind-event-listener@3.0.0: resolution: {integrity: sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==} - body-parser@2.2.0: - resolution: {integrity: sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==} - engines: {node: '>=18'} - brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} brace-expansion@2.0.2: resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - bytes@3.1.2: - resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} - engines: {node: '>= 0.8'} - - call-bind-apply-helpers@1.0.2: - resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} - engines: {node: '>= 0.4'} - - call-bound@1.0.4: - resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} - engines: {node: '>= 0.4'} - callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -1818,30 +1789,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - content-disposition@1.0.1: - resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} - engines: {node: '>=18'} - - content-type@1.0.5: - resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} - engines: {node: '>= 0.6'} - - cookie-signature@1.2.2: - resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} - engines: {node: '>=6.6.0'} - cookie@0.6.0: resolution: {integrity: sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==} engines: {node: '>= 0.6'} - cookie@0.7.2: - resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} - engines: {node: '>= 0.6'} - - cors@2.8.5: - resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} - engines: {node: '>= 0.10'} - crelt@1.0.6: resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} @@ -1883,10 +1834,6 @@ packages: resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==} engines: {node: '>=0.10.0'} - depd@2.0.0: - resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} - engines: {node: '>= 0.8'} - dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1899,21 +1846,21 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.5.0: - resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==} + devalue@5.6.0: + resolution: {integrity: sha512-BaD1s81TFFqbD6Uknni42TrolvEWA1Ih5L+OiHWmi4OYMJVwAYPGtha61I9KxTf52OvVHozHyjPu8zljqdF3uA==} devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} - dompurify@3.3.0: - resolution: {integrity: sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==} + dompurify@3.3.1: + resolution: {integrity: sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==} dotenv@17.2.3: resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} engines: {node: '>=12'} - drizzle-kit@0.31.7: - resolution: {integrity: sha512-hOzRGSdyKIU4FcTSFYGKdXEjFsncVwHZ43gY3WU5Bz9j5Iadp6Rh6hxLSQ1IWXpKLBKt/d5y1cpSPcV+FcoQ1A==} + drizzle-kit@0.31.8: + resolution: {integrity: sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==} hasBin: true drizzle-orm@0.44.7: @@ -2008,23 +1955,12 @@ packages: sqlite3: optional: true - dunder-proto@1.0.1: - resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} - engines: {node: '>= 0.4'} - - ee-first@1.1.1: - resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - - effect@3.19.6: - resolution: {integrity: sha512-Eh1E/CI+xCAcMSDC5DtyE29yWJINC0zwBbwHappQPorjKyS69rCA8qzpsHpfhKnPDYgxdg8zkknii8mZ+6YMQA==} + effect@3.19.9: + resolution: {integrity: sha512-taMXnfG/p+j7AmMOHHQaCHvjqwu9QBO3cxuZqL2dMG/yWcEMw0ZHruHe9B49OxtfKH/vKKDDKRhZ+1GJ2p5R5w==} emojilib@2.4.0: resolution: {integrity: sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==} - encodeurl@2.0.0: - resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} - engines: {node: '>= 0.8'} - enhanced-resolve@5.18.3: resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} engines: {node: '>=10.13.0'} @@ -2033,18 +1969,6 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} - es-define-property@1.0.1: - resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} - engines: {node: '>= 0.4'} - - es-errors@1.3.0: - resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} - engines: {node: '>= 0.4'} - - es-object-atoms@1.1.1: - resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} - engines: {node: '>= 0.4'} - esbuild-register@3.6.0: resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==} peerDependencies: @@ -2060,9 +1984,6 @@ packages: engines: {node: '>=18'} hasBin: true - escape-html@1.0.3: - resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2071,8 +1992,8 @@ packages: resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} engines: {node: '>=12'} - eslint-plugin-svelte@3.13.0: - resolution: {integrity: sha512-2ohCCQJJTNbIpQCSDSTWj+FN0OVfPmSO03lmSNT7ytqMaWF6kpT86LdzDqtm4sh7TVPl/OEWJ/d7R87bXP2Vjg==} + eslint-plugin-svelte@3.13.1: + resolution: {integrity: sha512-Ng+kV/qGS8P/isbNYVE3sJORtubB+yLEcYICMkUWNaDTb0SwZni/JhAYXh/Dz/q2eThUwWY0VMPZ//KYD1n3eQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.1 || ^9.0.0 @@ -2114,8 +2035,8 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} - esrap@2.1.3: - resolution: {integrity: sha512-T/Dhhv/QH+yYmiaLz9SA3PW+YyenlnRKDNdtlYJrSOBmNsH4nvPux+mTwx7p+wAedlJrGoZtXNI0a0MjQ2QkVg==} + esrap@2.2.1: + resolution: {integrity: sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==} esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} @@ -2129,28 +2050,6 @@ packages: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} - etag@1.8.1: - resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} - engines: {node: '>= 0.6'} - - eventsource-parser@3.0.6: - resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} - engines: {node: '>=18.0.0'} - - eventsource@3.0.7: - resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} - engines: {node: '>=18.0.0'} - - express-rate-limit@7.5.1: - resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} - engines: {node: '>= 16'} - peerDependencies: - express: '>= 4.11' - - express@5.1.0: - resolution: {integrity: sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==} - engines: {node: '>= 18'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -2161,13 +2060,6 @@ packages: fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - fast-diff@1.3.0: - resolution: {integrity: sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==} - - fast-glob@3.3.3: - resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -2177,9 +2069,6 @@ packages: fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fastq@1.19.1: - resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} - fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2197,14 +2086,6 @@ packages: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - finalhandler@2.1.0: - resolution: {integrity: sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==} - engines: {node: '>= 0.8'} - find-my-way-ts@0.1.6: resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} @@ -2223,14 +2104,6 @@ packages: resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==} engines: {node: '>=12.20.0'} - forwarded@0.2.0: - resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} - engines: {node: '>= 0.6'} - - fresh@2.0.0: - resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} - engines: {node: '>= 0.8'} - fsevents@2.3.2: resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2241,24 +2114,9 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - - get-intrinsic@1.3.0: - resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} - engines: {node: '>= 0.4'} - - get-proto@1.0.1: - resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} - engines: {node: '>= 0.4'} - get-tsconfig@4.13.0: resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -2271,40 +2129,13 @@ packages: resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==} engines: {node: '>=18'} - gopd@1.2.0: - resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} - engines: {node: '>= 0.4'} - graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - has-symbols@1.1.0: - resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} - engines: {node: '>= 0.4'} - - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - - http-errors@2.0.1: - resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} - engines: {node: '>= 0.8'} - - iconv-lite@0.6.3: - resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} - engines: {node: '>=0.10.0'} - - iconv-lite@0.7.0: - resolution: {integrity: sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==} - engines: {node: '>=0.10.0'} - ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -2321,13 +2152,6 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} - inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} - - ipaddr.js@1.9.1: - resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} - engines: {node: '>= 0.10'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2336,17 +2160,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - is-plain-obj@4.1.0: resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} engines: {node: '>=12'} - is-promise@4.0.0: - resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-reference@3.0.3: resolution: {integrity: sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==} @@ -2376,8 +2193,8 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} - katex@0.16.25: - resolution: {integrity: sha512-woHRUZ/iF23GBP1dkDQMh1QBad9dmr8/PAwNA54VrSOVYgI12MAcE14TqnDdQOdzyEonGzMepYnqBMYdsoAr8Q==} + katex@0.16.27: + resolution: {integrity: sha512-aeQoDkuRWSqQN6nSvVCEFvfXdqo1OQiCmmW1kc9xSdjutPv7BGO7pqY9sQRJpMOGrEdfDgF2TfRXe5eUAD2Waw==} hasBin: true keyv@4.5.4: @@ -2495,8 +2312,19 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - loro-crdt@1.10.0: - resolution: {integrity: sha512-Fms27q9IaDANUe5OACQL6qLMhJasMXzjRkyK+NAIiPQXGBK2VAp6C7pAr9fzuKbL71YyDgA4Pv69RGwiScWSPg==} + loro-codemirror@0.3.3: + resolution: {integrity: sha512-C6qAUmDjMTyoXVeDxKWixvr/TSTo/jXFrvgGW+wn6RAHeIzNFovBDLQNi/Sary4Ahx8DaYADTCm9eCaRAjvWtw==} + peerDependencies: + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.7.0 + loro-crdt: ^1.8.2 + + loro-crdt@1.10.3: + resolution: {integrity: sha512-vzWkVw7mWrKTilPjrgAhhzjAyOn3/DaUPJxdK9lunpEI1Y+uQMDBt/pEtRiovKFtGXo4tUVfULnFc7H/ufGwkQ==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2511,10 +2339,6 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} - math-intrinsics@1.1.0: - resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} - engines: {node: '>= 0.4'} - mdast-util-definitions@6.0.0: resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} @@ -2554,18 +2378,6 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} - media-typer@1.1.0: - resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} - engines: {node: '>= 0.8'} - - merge-descriptors@2.0.0: - resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} - engines: {node: '>=18'} - - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - micromark-core-commonmark@2.0.3: resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} @@ -2650,18 +2462,6 @@ packages: micromark@4.0.2: resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - - mime-db@1.54.0: - resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} - engines: {node: '>= 0.6'} - - mime-types@3.0.2: - resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} - engines: {node: '>=18'} - minimatch@3.1.2: resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} @@ -2703,10 +2503,6 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - negotiator@1.0.0: - resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} - engines: {node: '>= 0.6'} - node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} @@ -2724,21 +2520,6 @@ packages: resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} hasBin: true - object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - - object-inspect@1.13.4: - resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} - engines: {node: '>= 0.4'} - - on-finished@2.4.1: - resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} - engines: {node: '>= 0.8'} - - once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} - optionator@0.9.4: resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} engines: {node: '>= 0.8.0'} @@ -2758,10 +2539,6 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} - parseurl@1.3.3: - resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} - engines: {node: '>= 0.8'} - path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -2770,31 +2547,20 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-to-regexp@8.3.0: - resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} - picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} - picomatch@4.0.3: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} - pkce-challenge@5.0.0: - resolution: {integrity: sha512-ueGLflrrnvwB3xuo/uGob5pd5FN7l0MsLf0Z87o/UQmRtwjvfylfc9MurIxRAWywCYTgrvpXBcqjV4OfCYGCIQ==} - engines: {node: '>=16.20.0'} - - playwright-core@1.56.1: - resolution: {integrity: sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ==} + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} engines: {node: '>=18'} hasBin: true - playwright@1.56.1: - resolution: {integrity: sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw==} + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} engines: {node: '>=18'} hasBin: true @@ -2826,8 +2592,8 @@ packages: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} - postcss-selector-parser@7.1.0: - resolution: {integrity: sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==} + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} engines: {node: '>=4'} postcss@8.5.6: @@ -2844,8 +2610,8 @@ packages: prettier: ^3.0.0 svelte: ^3.2.0 || ^4.0.0-next.0 || ^5.0.0-next.0 - prettier-plugin-tailwindcss@0.7.1: - resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==} + prettier-plugin-tailwindcss@0.7.2: + resolution: {integrity: sha512-LkphyK3Fw+q2HdMOoiEHWf93fNtYJwfamoKPl7UwtjFQdei/iIBoX11G6j706FzN3ymX9mPVi97qIY8328vdnA==} engines: {node: '>=20.19'} peerDependencies: '@ianvs/prettier-plugin-sort-imports': '*' @@ -2899,8 +2665,8 @@ packages: prettier-plugin-svelte: optional: true - prettier@3.6.2: - resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + prettier@3.7.4: + resolution: {integrity: sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==} engines: {node: '>=14'} hasBin: true @@ -2952,8 +2718,8 @@ packages: prosemirror-state@1.4.4: resolution: {integrity: sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==} - prosemirror-tables@1.8.1: - resolution: {integrity: sha512-DAgDoUYHCcc6tOGpLVPSU1k84kCUWTWnfWX3UDy2Delv4ryH0KqTD6RBI6k4yi9j9I8gl3j8MkPpRD/vWPZbug==} + prosemirror-tables@1.8.3: + resolution: {integrity: sha512-wbqCR/RlRPRe41a4LFtmhKElzBEfBTdtAYWNIGHM6X2e24NN/MTNUKyXjjphfAfdQce37Kh/5yf765mLPYDe7Q==} prosemirror-trailing-node@3.0.0: resolution: {integrity: sha512-xiun5/3q0w5eRnGYfNlW1uU9W6x5MoFKWwq/0TIRgt09lv7Hcser2QYV8t4muXbEr+Fwo0geYn79Xs4GKywrRQ==} @@ -2965,12 +2731,8 @@ packages: prosemirror-transform@1.10.5: resolution: {integrity: sha512-RPDQCxIDhIBb1o36xxwsaeAvivO8VLJcgBtzmOwQ64bMtsVFh5SSuJ6dWSxO1UsHTiTXPCgQm3PDJt7p6IOLbw==} - prosemirror-view@1.41.3: - resolution: {integrity: sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==} - - proxy-addr@2.0.7: - resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} - engines: {node: '>= 0.10'} + prosemirror-view@1.41.4: + resolution: {integrity: sha512-WkKgnyjNncri03Gjaz3IFWvCAE94XoiEgvtr0/r2Xw7R8/IjK3sKLSiDoCHWcsXSAinVaKlGRZDvMCsF1kbzjA==} punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} @@ -2983,24 +2745,9 @@ packages: pure-rand@6.1.0: resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} - qs@6.14.0: - resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} - engines: {node: '>=0.6'} - - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - raf-schd@4.0.3: resolution: {integrity: sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==} - range-parser@1.2.1: - resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} - engines: {node: '>= 0.6'} - - raw-body@3.0.2: - resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} - engines: {node: '>= 0.10'} - readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -3031,10 +2778,6 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - reusify@1.1.0: - resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - rollup@4.53.3: resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3043,20 +2786,22 @@ packages: rope-sequence@1.3.4: resolution: {integrity: sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==} - router@2.2.0: - resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} - engines: {node: '>= 18'} - - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + runed@0.37.0: + resolution: {integrity: sha512-zphHjvLZEpcJiV3jezT96SnNwePaUIEd1HEMuPGZ6DwOMao9S2ZAUCYJPKquRM5J22AwAOpGj0KmxOkQdkBfwQ==} + peerDependencies: + '@sveltejs/kit': ^2.21.0 + svelte: ^5.7.0 + zod: ^4.1.0 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + zod: + optional: true sade@1.8.1: resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} engines: {node: '>=6'} - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - scule@1.3.0: resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} @@ -3065,20 +2810,9 @@ packages: engines: {node: '>=10'} hasBin: true - send@1.2.0: - resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==} - engines: {node: '>= 18'} - - serve-static@2.2.0: - resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==} - engines: {node: '>= 18'} - set-cookie-parser@2.7.2: resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} - setprototypeof@1.2.0: - resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3087,22 +2821,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - side-channel-list@1.0.0: - resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} - engines: {node: '>= 0.4'} - - side-channel-map@1.0.1: - resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} - engines: {node: '>= 0.4'} - - side-channel-weakmap@1.0.2: - resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} - engines: {node: '>= 0.4'} - - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} - engines: {node: '>= 0.4'} - sirv@3.0.2: resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} engines: {node: '>=18'} @@ -3122,10 +2840,6 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - statuses@2.0.2: - resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} - engines: {node: '>= 0.8'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -3145,9 +2859,9 @@ packages: svelte: ^4.0.0 || ^5.0.0-next.0 typescript: '>=5.0.0' - svelte-eslint-parser@1.4.0: - resolution: {integrity: sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.18.3} + svelte-eslint-parser@1.4.1: + resolution: {integrity: sha512-1eqkfQ93goAhjAXxZiu1SaKI9+0/sxp4JIWQwUpsz7ybehRE5L8dNuz7Iry7K22R47p5/+s9EM+38nHV2OlgXA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0, pnpm: 10.24.0} peerDependencies: svelte: ^3.37.0 || ^4.0.0 || ^5.0.0 peerDependenciesMeta: @@ -3160,8 +2874,8 @@ packages: svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 typescript: ^4.9.4 || ^5.0.0 - svelte@5.44.0: - resolution: {integrity: sha512-R7387No2zEGw4CtYtI2rgsui6BqjFARzoZFGLiLN5OPla0Pq4Ra2WwcP/zBomP3MYalhSNvF1fzDMuU0P0zPJw==} + svelte@5.45.8: + resolution: {integrity: sha512-1Jh7FwVh/2Uxg0T7SeE1qFKMhwYH45b2v53bcZpW7qHa6O8iU1ByEj56PF0IQ6dU4HE5gRkic6h+vx+tclHeiw==} engines: {node: '>=18'} tailwindcss@4.1.17: @@ -3171,6 +2885,12 @@ packages: resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==} engines: {node: '>=6'} + temporal-polyfill@0.3.0: + resolution: {integrity: sha512-qNsTkX9K8hi+FHDfHmf22e/OGuXmfBm9RqNismxBrnSmZVJKegQ+HYYXT+R7Ha8F/YSm2Y34vmzD4cxMu2u95g==} + + temporal-spec@0.3.0: + resolution: {integrity: sha512-n+noVpIqz4hYgFSMOSiINNOUOMFtV5cZQNCmmszA6GiVFVRt3G7AqVyhXjhCSmowvQn+NsGn+jMDMKJYHd3bSQ==} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} @@ -3180,14 +2900,6 @@ packages: peerDependencies: '@tiptap/core': ^3.0.1 - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} - - toidentifier@1.0.1: - resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} - engines: {node: '>=0.6'} - totalist@3.0.1: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} @@ -3211,12 +2923,8 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-is@2.0.1: - resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} - engines: {node: '>= 0.6'} - - typescript-eslint@8.47.0: - resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==} + typescript-eslint@8.49.0: + resolution: {integrity: sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 @@ -3255,10 +2963,6 @@ packages: unist-util-visit@5.0.0: resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} - unpipe@1.0.0: - resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} - engines: {node: '>= 0.8'} - uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -3273,10 +2977,6 @@ packages: resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} hasBin: true - vary@1.1.2: - resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} - engines: {node: '>= 0.8'} - vfile-message@4.0.3: resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} @@ -3298,8 +2998,8 @@ packages: peerDependencies: vite: ^2 || ^3 || ^4 || ^5 || ^6 || ^7 - vite@7.2.4: - resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} + vite@7.2.7: + resolution: {integrity: sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: @@ -3362,9 +3062,6 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} - wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.18.3: resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} engines: {node: '>=10.0.0'} @@ -3381,11 +3078,6 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} - yaml@2.8.1: - resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} - engines: {node: '>= 14.6'} - hasBin: true - yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -3393,13 +3085,8 @@ packages: zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} - zod-to-json-schema@3.25.0: - resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} - peerDependencies: - zod: ^3.25 || ^4 - - zod@3.25.76: - resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.1.13: + resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -3419,9 +3106,9 @@ snapshots: '@babel/runtime@7.28.4': {} - '@benrbray/prosemirror-math@1.0.0(katex@0.16.25)(prosemirror-commands@1.7.1)(prosemirror-history@1.5.0)(prosemirror-inputrules@1.5.1)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-transform@1.10.5)(prosemirror-view@1.41.3)': + '@benrbray/prosemirror-math@1.0.0(katex@0.16.27)(prosemirror-commands@1.7.1)(prosemirror-history@1.5.0)(prosemirror-inputrules@1.5.1)(prosemirror-keymap@1.2.3)(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-transform@1.10.5)(prosemirror-view@1.41.4)': dependencies: - katex: 0.16.25 + katex: 0.16.27 prosemirror-commands: 1.7.1 prosemirror-history: 1.5.0 prosemirror-inputrules: 1.5.1 @@ -3429,30 +3116,30 @@ snapshots: prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.3 + prosemirror-view: 1.41.4 '@codemirror/autocomplete@6.20.0': dependencies: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 '@codemirror/commands@6.10.0': dependencies: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 '@codemirror/lang-angular@0.1.4': dependencies: '@codemirror/lang-html': 6.4.11 '@codemirror/lang-javascript': 6.2.4 '@codemirror/language': 6.11.3 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@codemirror/lang-cpp@6.0.3': dependencies: @@ -3464,7 +3151,7 @@ snapshots: '@codemirror/autocomplete': 6.20.0 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/css': 1.3.0 '@codemirror/lang-go@6.0.1': @@ -3472,7 +3159,7 @@ snapshots: '@codemirror/autocomplete': 6.20.0 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/go': 1.0.1 '@codemirror/lang-html@6.4.11': @@ -3482,8 +3169,8 @@ snapshots: '@codemirror/lang-javascript': 6.2.4 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 '@lezer/css': 1.3.0 '@lezer/html': 1.3.12 @@ -3498,17 +3185,17 @@ snapshots: '@codemirror/language': 6.11.3 '@codemirror/lint': 6.9.2 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 '@lezer/javascript': 1.5.4 '@codemirror/lang-jinja@6.0.0': dependencies: '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.11.3 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@codemirror/lang-json@6.0.2': dependencies: @@ -3519,9 +3206,9 @@ snapshots: dependencies: '@codemirror/lang-css': 6.3.1 '@codemirror/language': 6.11.3 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@codemirror/lang-liquid@6.3.0': dependencies: @@ -3529,10 +3216,10 @@ snapshots: '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@codemirror/lang-markdown@6.5.0': dependencies: @@ -3540,16 +3227,16 @@ snapshots: '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 - '@lezer/markdown': 1.6.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 + '@lezer/markdown': 1.6.1 '@codemirror/lang-php@6.0.2': dependencies: '@codemirror/lang-html': 6.4.11 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/php': 1.0.5 '@codemirror/lang-python@6.2.1': @@ -3557,7 +3244,7 @@ snapshots: '@codemirror/autocomplete': 6.20.0 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/python': 1.1.18 '@codemirror/lang-rust@6.0.2': @@ -3570,7 +3257,7 @@ snapshots: '@codemirror/lang-css': 6.3.1 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/sass': 1.1.0 '@codemirror/lang-sql@6.10.0': @@ -3578,33 +3265,33 @@ snapshots: '@codemirror/autocomplete': 6.20.0 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@codemirror/lang-vue@0.1.3': dependencies: '@codemirror/lang-html': 6.4.11 '@codemirror/lang-javascript': 6.2.4 '@codemirror/language': 6.11.3 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@codemirror/lang-wast@6.0.2': dependencies: '@codemirror/language': 6.11.3 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@codemirror/lang-xml@6.1.0': dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 '@lezer/xml': 1.0.6 '@codemirror/lang-yaml@6.1.2': @@ -3612,9 +3299,9 @@ snapshots: '@codemirror/autocomplete': 6.20.0 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/yaml': 1.0.3 '@codemirror/language-data@6.5.2': @@ -3646,10 +3333,10 @@ snapshots: '@codemirror/language@6.11.3': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 style-mod: 4.1.3 '@codemirror/legacy-modes@6.5.2': @@ -3659,13 +3346,13 @@ snapshots: '@codemirror/lint@6.9.2': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 + '@codemirror/view': 6.39.0 crelt: 1.0.6 '@codemirror/search@6.5.11': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 + '@codemirror/view': 6.39.0 crelt: 1.0.6 '@codemirror/state@6.5.2': @@ -3676,10 +3363,10 @@ snapshots: dependencies: '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 + '@codemirror/view': 6.39.0 '@lezer/highlight': 1.2.3 - '@codemirror/view@6.38.8': + '@codemirror/view@6.39.0': dependencies: '@codemirror/state': 6.5.2 crelt: 1.0.6 @@ -3690,9 +3377,9 @@ snapshots: '@effect/language-service@0.56.0': {} - '@effect/platform@0.93.3(effect@3.19.6)': + '@effect/platform@0.93.6(effect@3.19.9)': dependencies: - effect: 3.19.6 + effect: 3.19.9 find-my-way-ts: 0.1.6 msgpackr: 1.11.5 multipasta: 0.2.7 @@ -3896,7 +3583,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.1': + '@eslint/eslintrc@3.3.3': dependencies: ajv: 6.12.6 debug: 4.4.3 @@ -3919,6 +3606,16 @@ snapshots: '@eslint/core': 0.17.0 levn: 0.4.1 + '@hpke/chacha20poly1305@1.7.1': + dependencies: + '@hpke/common': 1.8.1 + + '@hpke/common@1.8.1': {} + + '@hpke/core@1.7.5': + dependencies: + '@hpke/common': 1.8.1 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.7': @@ -3949,98 +3646,98 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@lezer/common@1.3.0': {} + '@lezer/common@1.4.0': {} '@lezer/cpp@1.1.3': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/css@1.3.0': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/go@1.0.1': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/highlight@1.2.3': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/html@1.3.12': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/java@1.1.3': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/javascript@1.5.4': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/json@1.0.3': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 - '@lezer/lr@1.4.3': + '@lezer/lr@1.4.4': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 - '@lezer/markdown@1.6.0': + '@lezer/markdown@1.6.1': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 '@lezer/php@1.0.5': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/python@1.1.18': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/rust@1.0.2': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/sass@1.1.0': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/xml@1.0.6': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@lezer/yaml@1.0.3': dependencies: - '@lezer/common': 1.3.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/lr': 1.4.3 + '@lezer/lr': 1.4.4 '@libsql/client@0.15.15': dependencies: @@ -4104,95 +3801,95 @@ snapshots: '@libsql/win32-x64-msvc@0.5.22': optional: true - '@lucide/svelte@0.554.0(svelte@5.44.0)': + '@lucide/svelte@0.554.0(svelte@5.45.8)': dependencies: - svelte: 5.44.0 + svelte: 5.45.8 '@marijn/find-cluster-break@1.0.2': {} - '@milkdown-lab/plugin-split-editing@1.3.1(@milkdown/core@7.17.1)(@milkdown/prose@7.17.1)': + '@milkdown-lab/plugin-split-editing@1.3.1(@milkdown/core@7.17.3)(@milkdown/prose@7.17.3)': dependencies: '@codemirror/commands': 6.10.0 '@codemirror/lang-markdown': 6.5.0 '@codemirror/language': 6.11.3 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@milkdown/core': 7.17.1 - '@milkdown/ctx': 7.17.1 - '@milkdown/prose': 7.17.1 - '@milkdown/utils': 7.17.1 + '@codemirror/view': 6.39.0 + '@milkdown/core': 7.17.3 + '@milkdown/ctx': 7.17.3 + '@milkdown/prose': 7.17.3 + '@milkdown/utils': 7.17.3 style-mod: 4.1.3 transitivePeerDependencies: - supports-color - '@milkdown/core@7.17.1': + '@milkdown/core@7.17.3': dependencies: - '@milkdown/ctx': 7.17.1 - '@milkdown/exception': 7.17.1 - '@milkdown/prose': 7.17.1 - '@milkdown/transformer': 7.17.1 + '@milkdown/ctx': 7.17.3 + '@milkdown/exception': 7.17.3 + '@milkdown/prose': 7.17.3 + '@milkdown/transformer': 7.17.3 remark-parse: 11.0.0 remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - '@milkdown/ctx@7.17.1': + '@milkdown/ctx@7.17.3': dependencies: - '@milkdown/exception': 7.17.1 + '@milkdown/exception': 7.17.3 - '@milkdown/exception@7.17.1': {} + '@milkdown/exception@7.17.3': {} - '@milkdown/plugin-history@7.17.1': + '@milkdown/plugin-history@7.17.3': dependencies: - '@milkdown/core': 7.17.1 - '@milkdown/ctx': 7.17.1 - '@milkdown/prose': 7.17.1 - '@milkdown/utils': 7.17.1 + '@milkdown/core': 7.17.3 + '@milkdown/ctx': 7.17.3 + '@milkdown/prose': 7.17.3 + '@milkdown/utils': 7.17.3 transitivePeerDependencies: - supports-color - '@milkdown/plugin-listener@7.17.1': + '@milkdown/plugin-listener@7.17.3': dependencies: - '@milkdown/core': 7.17.1 - '@milkdown/ctx': 7.17.1 - '@milkdown/prose': 7.17.1 + '@milkdown/core': 7.17.3 + '@milkdown/ctx': 7.17.3 + '@milkdown/prose': 7.17.3 '@types/lodash-es': 4.17.12 lodash-es: 4.17.21 transitivePeerDependencies: - supports-color - '@milkdown/preset-commonmark@7.17.1': + '@milkdown/preset-commonmark@7.17.3': dependencies: - '@milkdown/core': 7.17.1 - '@milkdown/ctx': 7.17.1 - '@milkdown/exception': 7.17.1 - '@milkdown/prose': 7.17.1 - '@milkdown/transformer': 7.17.1 - '@milkdown/utils': 7.17.1 + '@milkdown/core': 7.17.3 + '@milkdown/ctx': 7.17.3 + '@milkdown/exception': 7.17.3 + '@milkdown/prose': 7.17.3 + '@milkdown/transformer': 7.17.3 + '@milkdown/utils': 7.17.3 remark-inline-links: 7.0.0 unist-util-visit: 5.0.0 unist-util-visit-parents: 6.0.2 transitivePeerDependencies: - supports-color - '@milkdown/preset-gfm@7.17.1': + '@milkdown/preset-gfm@7.17.3': dependencies: - '@milkdown/core': 7.17.1 - '@milkdown/ctx': 7.17.1 - '@milkdown/exception': 7.17.1 - '@milkdown/preset-commonmark': 7.17.1 - '@milkdown/prose': 7.17.1 - '@milkdown/transformer': 7.17.1 - '@milkdown/utils': 7.17.1 + '@milkdown/core': 7.17.3 + '@milkdown/ctx': 7.17.3 + '@milkdown/exception': 7.17.3 + '@milkdown/preset-commonmark': 7.17.3 + '@milkdown/prose': 7.17.3 + '@milkdown/transformer': 7.17.3 + '@milkdown/utils': 7.17.3 prosemirror-safari-ime-span: 1.0.2 remark-gfm: 4.0.1 transitivePeerDependencies: - supports-color - '@milkdown/prose@7.17.1': + '@milkdown/prose@7.17.3': dependencies: - '@milkdown/exception': 7.17.1 + '@milkdown/exception': 7.17.3 prosemirror-changeset: 2.3.1 prosemirror-commands: 1.7.1 prosemirror-dropcursor: 1.8.2 @@ -4203,59 +3900,41 @@ snapshots: prosemirror-model: 1.25.4 prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.4 - prosemirror-tables: 1.8.1 + prosemirror-tables: 1.8.3 prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.3 + prosemirror-view: 1.41.4 - '@milkdown/theme-nord@7.17.1': + '@milkdown/theme-nord@7.17.3': dependencies: - '@milkdown/core': 7.17.1 - '@milkdown/ctx': 7.17.1 - '@milkdown/prose': 7.17.1 + '@milkdown/core': 7.17.3 + '@milkdown/ctx': 7.17.3 + '@milkdown/prose': 7.17.3 clsx: 2.1.1 transitivePeerDependencies: - supports-color - '@milkdown/transformer@7.17.1': + '@milkdown/transformer@7.17.3': dependencies: - '@milkdown/exception': 7.17.1 - '@milkdown/prose': 7.17.1 + '@milkdown/exception': 7.17.3 + '@milkdown/prose': 7.17.3 remark: 15.0.1 unified: 11.0.5 transitivePeerDependencies: - supports-color - '@milkdown/utils@7.17.1': + '@milkdown/utils@7.17.3': dependencies: - '@milkdown/core': 7.17.1 - '@milkdown/ctx': 7.17.1 - '@milkdown/exception': 7.17.1 - '@milkdown/prose': 7.17.1 - '@milkdown/transformer': 7.17.1 + '@milkdown/core': 7.17.3 + '@milkdown/ctx': 7.17.3 + '@milkdown/exception': 7.17.3 + '@milkdown/prose': 7.17.3 + '@milkdown/transformer': 7.17.3 nanoid: 5.1.6 transitivePeerDependencies: - supports-color '@mixmark-io/domino@2.2.0': {} - '@modelcontextprotocol/sdk@1.22.0': - dependencies: - ajv: 8.17.1 - ajv-formats: 3.0.1(ajv@8.17.1) - content-type: 1.0.5 - cors: 2.8.5 - cross-spawn: 7.0.6 - eventsource: 3.0.7 - eventsource-parser: 3.0.6 - express: 5.1.0 - express-rate-limit: 7.5.1(express@5.1.0) - pkce-challenge: 5.0.0 - raw-body: 3.0.2 - zod: 3.25.76 - zod-to-json-schema: 3.25.0(zod@3.25.76) - transitivePeerDependencies: - - supports-color - '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': optional: true @@ -4344,18 +4023,6 @@ snapshots: '@node-rs/argon2-win32-ia32-msvc': 2.0.2 '@node-rs/argon2-win32-x64-msvc': 2.0.2 - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 - - '@nodelib/fs.stat@2.0.5': {} - - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.19.1 - '@oslojs/asn1@1.0.0': dependencies: '@oslojs/binary': 1.0.0 @@ -4371,7 +4038,7 @@ snapshots: '@polka/url@1.0.0-next.29': {} - '@prosemark/core@0.0.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.0)(@codemirror/lang-markdown@6.5.0)(@codemirror/language-data@6.5.2)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3)': + '@prosemark/core@0.0.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.0)(@codemirror/lang-markdown@6.5.0)(@codemirror/language-data@6.5.2)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.39.0)(@lezer/highlight@1.2.3)': dependencies: '@codemirror/autocomplete': 6.20.0 '@codemirror/commands': 6.10.0 @@ -4381,27 +4048,27 @@ snapshots: '@codemirror/lint': 6.9.2 '@codemirror/search': 6.5.11 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 '@lezer/highlight': 1.2.3 - '@lezer/markdown': 1.6.0 + '@lezer/markdown': 1.6.1 ajv: 8.17.1 node-emoji: 2.2.0 '@prosemark/paste-rich-text@0.0.2': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 + '@codemirror/view': 6.39.0 '@types/turndown': 5.0.6 turndown: 7.2.2 '@prosemark/render-html@0.0.5(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.0)(@codemirror/lang-markdown@6.5.0)(@codemirror/language-data@6.5.2)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@lezer/highlight@1.2.3)': dependencies: '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 - '@lezer/common': 1.3.0 - '@prosemark/core': 0.0.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.0)(@codemirror/lang-markdown@6.5.0)(@codemirror/language-data@6.5.2)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.38.8)(@lezer/highlight@1.2.3) - dompurify: 3.3.0 + '@codemirror/view': 6.39.0 + '@lezer/common': 1.4.0 + '@prosemark/core': 0.0.4(@codemirror/autocomplete@6.20.0)(@codemirror/commands@6.10.0)(@codemirror/lang-markdown@6.5.0)(@codemirror/language-data@6.5.2)(@codemirror/language@6.11.3)(@codemirror/lint@6.9.2)(@codemirror/search@6.5.11)(@codemirror/state@6.5.2)(@codemirror/view@6.39.0)(@lezer/highlight@1.2.3) + dompurify: 3.3.1 transitivePeerDependencies: - '@codemirror/autocomplete' - '@codemirror/commands' @@ -4488,23 +4155,23 @@ snapshots: '@standard-schema/spec@1.0.0': {} - '@sveltejs/acorn-typescript@1.0.7(acorn@8.15.0)': + '@sveltejs/acorn-typescript@1.0.8(acorn@8.15.0)': dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)))': + '@sveltejs/adapter-auto@7.0.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))': dependencies: - '@sveltejs/kit': 2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) - '@sveltejs/kit@2.49.0(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1))': + '@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@standard-schema/spec': 1.0.0 - '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 - devalue: 5.5.0 + devalue: 5.6.0 esm-env: 1.2.2 kleur: 4.1.5 magic-string: 0.30.21 @@ -4512,27 +4179,27 @@ snapshots: sade: 1.8.1 set-cookie-parser: 2.7.2 sirv: 3.0.2 - svelte: 5.44.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + svelte: 5.45.8 + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) - '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte-inspector@5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: - '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) debug: 4.4.3 - svelte: 5.44.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + svelte: 5.45.8 + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)))(svelte@5.44.0)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte-inspector': 5.0.1(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) debug: 4.4.3 deepmerge: 4.3.1 magic-string: 0.30.21 - svelte: 5.44.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)) + svelte: 5.45.8 + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) + vitefu: 1.1.1(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) transitivePeerDependencies: - supports-color @@ -4656,122 +4323,122 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 4.1.17 - '@tailwindcss/vite@4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1))': + '@tailwindcss/vite@4.1.17(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))': dependencies: '@tailwindcss/node': 4.1.17 '@tailwindcss/oxide': 4.1.17 tailwindcss: 4.1.17 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) - '@tiptap/core@3.11.0(@tiptap/pm@3.11.0)': + '@tiptap/core@3.13.0(@tiptap/pm@3.13.0)': dependencies: - '@tiptap/pm': 3.11.0 + '@tiptap/pm': 3.13.0 - '@tiptap/extension-blockquote@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-blockquote@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-bold@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-bold@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-bullet-list@3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + '@tiptap/extension-bullet-list@3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) - '@tiptap/extension-code-block@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + '@tiptap/extension-code-block@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) - '@tiptap/pm': 3.11.0 + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) + '@tiptap/pm': 3.13.0 - '@tiptap/extension-code@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-code@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-document@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-document@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-dropcursor@3.11.0(@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + '@tiptap/extension-dropcursor@3.13.0(@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/extensions': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extensions': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) - '@tiptap/extension-gapcursor@3.11.0(@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + '@tiptap/extension-gapcursor@3.13.0(@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/extensions': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extensions': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) - '@tiptap/extension-hard-break@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-hard-break@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-heading@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-heading@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-horizontal-rule@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + '@tiptap/extension-horizontal-rule@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) - '@tiptap/pm': 3.11.0 + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) + '@tiptap/pm': 3.13.0 - '@tiptap/extension-italic@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-italic@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-link@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + '@tiptap/extension-link@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) - '@tiptap/pm': 3.11.0 + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) + '@tiptap/pm': 3.13.0 linkifyjs: 4.3.2 - '@tiptap/extension-list-item@3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + '@tiptap/extension-list-item@3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) - '@tiptap/extension-list-keymap@3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + '@tiptap/extension-list-keymap@3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) - '@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + '@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) - '@tiptap/pm': 3.11.0 + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) + '@tiptap/pm': 3.13.0 - '@tiptap/extension-mathematics@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)(katex@0.16.25)': + '@tiptap/extension-mathematics@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)(katex@0.16.27)': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) - '@tiptap/pm': 3.11.0 - katex: 0.16.25 + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) + '@tiptap/pm': 3.13.0 + katex: 0.16.27 - '@tiptap/extension-ordered-list@3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0))': + '@tiptap/extension-ordered-list@3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) + '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) - '@tiptap/extension-paragraph@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-paragraph@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-strike@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-strike@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-text@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-text@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-typography@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-typography@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extension-underline@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))': + '@tiptap/extension-underline@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) - '@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)': + '@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)': dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) - '@tiptap/pm': 3.11.0 + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) + '@tiptap/pm': 3.13.0 - '@tiptap/pm@3.11.0': + '@tiptap/pm@3.13.0': dependencies: prosemirror-changeset: 2.3.1 prosemirror-collab: 1.3.1 @@ -4787,37 +4454,37 @@ snapshots: prosemirror-schema-basic: 1.2.4 prosemirror-schema-list: 1.5.1 prosemirror-state: 1.4.4 - prosemirror-tables: 1.8.1 - prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3) + prosemirror-tables: 1.8.3 + prosemirror-trailing-node: 3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4) prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.3 - - '@tiptap/starter-kit@3.11.0': - dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) - '@tiptap/extension-blockquote': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-bold': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-bullet-list': 3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) - '@tiptap/extension-code': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-code-block': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) - '@tiptap/extension-document': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-dropcursor': 3.11.0(@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) - '@tiptap/extension-gapcursor': 3.11.0(@tiptap/extensions@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) - '@tiptap/extension-hard-break': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-heading': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-horizontal-rule': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) - '@tiptap/extension-italic': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-link': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) - '@tiptap/extension-list': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) - '@tiptap/extension-list-item': 3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) - '@tiptap/extension-list-keymap': 3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) - '@tiptap/extension-ordered-list': 3.11.0(@tiptap/extension-list@3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0)) - '@tiptap/extension-paragraph': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-strike': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-text': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extension-underline': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)) - '@tiptap/extensions': 3.11.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0))(@tiptap/pm@3.11.0) - '@tiptap/pm': 3.11.0 + prosemirror-view: 1.41.4 + + '@tiptap/starter-kit@3.13.0': + dependencies: + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) + '@tiptap/extension-blockquote': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-bold': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-bullet-list': 3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)) + '@tiptap/extension-code': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-code-block': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) + '@tiptap/extension-document': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-dropcursor': 3.13.0(@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)) + '@tiptap/extension-gapcursor': 3.13.0(@tiptap/extensions@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)) + '@tiptap/extension-hard-break': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-heading': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-horizontal-rule': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) + '@tiptap/extension-italic': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-link': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) + '@tiptap/extension-list': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) + '@tiptap/extension-list-item': 3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)) + '@tiptap/extension-list-keymap': 3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)) + '@tiptap/extension-ordered-list': 3.13.0(@tiptap/extension-list@3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0)) + '@tiptap/extension-paragraph': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-strike': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-text': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extension-underline': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)) + '@tiptap/extensions': 3.13.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0))(@tiptap/pm@3.13.0) + '@tiptap/pm': 3.13.0 '@tybys/wasm-util@0.10.1': dependencies: @@ -4879,16 +4546,15 @@ snapshots: dependencies: '@types/node': 24.10.1 - '@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/eslint-plugin@8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/type-utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/type-utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.49.0 eslint: 9.39.1(jiti@2.6.1) - graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 ts-api-utils: 2.1.0(typescript@5.9.3) @@ -4896,41 +4562,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.47.0(typescript@5.9.3)': + '@typescript-eslint/project-service@8.49.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) - '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.47.0': + '@typescript-eslint/scope-manager@8.49.0': dependencies: - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/visitor-keys': 8.49.0 - '@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.49.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) debug: 4.4.3 eslint: 9.39.1(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.9.3) @@ -4938,55 +4604,45 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.47.0': {} + '@typescript-eslint/types@8.49.0': {} - '@typescript-eslint/typescript-estree@8.47.0(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.49.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.47.0(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3) - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/visitor-keys': 8.47.0 + '@typescript-eslint/project-service': 8.49.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.49.0(typescript@5.9.3) + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/visitor-keys': 8.49.0 debug: 4.4.3 - fast-glob: 3.3.3 - is-glob: 4.0.3 minimatch: 9.0.5 semver: 7.7.3 + tinyglobby: 0.2.15 ts-api-utils: 2.1.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': + '@typescript-eslint/utils@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)': dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/types': 8.47.0 - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) + '@typescript-eslint/scope-manager': 8.49.0 + '@typescript-eslint/types': 8.49.0 + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.47.0': + '@typescript-eslint/visitor-keys@8.49.0': dependencies: - '@typescript-eslint/types': 8.47.0 + '@typescript-eslint/types': 8.49.0 eslint-visitor-keys: 4.2.1 - accepts@2.0.0: - dependencies: - mime-types: 3.0.2 - negotiator: 1.0.0 - acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 acorn@8.15.0: {} - ajv-formats@3.0.1(ajv@8.17.1): - optionalDependencies: - ajv: 8.17.1 - ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5019,20 +4675,6 @@ snapshots: bind-event-listener@3.0.0: {} - body-parser@2.2.0: - dependencies: - bytes: 3.1.2 - content-type: 1.0.5 - debug: 4.4.3 - http-errors: 2.0.1 - iconv-lite: 0.6.3 - on-finished: 2.4.1 - qs: 6.14.0 - raw-body: 3.0.2 - type-is: 2.0.1 - transitivePeerDependencies: - - supports-color - brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -5042,24 +4684,8 @@ snapshots: dependencies: balanced-match: 1.0.2 - braces@3.0.3: - dependencies: - fill-range: 7.1.1 - buffer-from@1.1.2: {} - bytes@3.1.2: {} - - call-bind-apply-helpers@1.0.2: - dependencies: - es-errors: 1.3.0 - function-bind: 1.1.2 - - call-bound@1.0.4: - dependencies: - call-bind-apply-helpers: 1.0.2 - get-intrinsic: 1.3.0 - callsites@3.1.0: {} ccount@2.0.1: {} @@ -5087,7 +4713,7 @@ snapshots: '@codemirror/lint': 6.9.2 '@codemirror/search': 6.5.11 '@codemirror/state': 6.5.2 - '@codemirror/view': 6.38.8 + '@codemirror/view': 6.39.0 color-convert@2.0.1: dependencies: @@ -5099,21 +4725,8 @@ snapshots: concat-map@0.0.1: {} - content-disposition@1.0.1: {} - - content-type@1.0.5: {} - - cookie-signature@1.2.2: {} - cookie@0.6.0: {} - cookie@0.7.2: {} - - cors@2.8.5: - dependencies: - object-assign: 4.1.1 - vary: 1.1.2 - crelt@1.0.6: {} cross-spawn@7.0.6: @@ -5142,27 +4755,25 @@ snapshots: deepmerge@4.3.1: {} - depd@2.0.0: {} - dequal@2.0.3: {} detect-libc@2.0.2: {} detect-libc@2.1.2: {} - devalue@5.5.0: {} + devalue@5.6.0: {} devlop@1.1.0: dependencies: dequal: 2.0.3 - dompurify@3.3.0: + dompurify@3.3.1: optionalDependencies: '@types/trusted-types': 2.0.7 dotenv@17.2.3: {} - drizzle-kit@0.31.7: + drizzle-kit@0.31.8: dependencies: '@drizzle-team/brocli': 0.10.2 '@esbuild-kit/esm-loader': 2.6.5 @@ -5175,23 +4786,13 @@ snapshots: optionalDependencies: '@libsql/client': 0.15.15 - dunder-proto@1.0.1: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-errors: 1.3.0 - gopd: 1.2.0 - - ee-first@1.1.1: {} - - effect@3.19.6: + effect@3.19.9: dependencies: '@standard-schema/spec': 1.0.0 fast-check: 3.23.2 emojilib@2.4.0: {} - encodeurl@2.0.0: {} - enhanced-resolve@5.18.3: dependencies: graceful-fs: 4.2.11 @@ -5199,14 +4800,6 @@ snapshots: entities@4.5.0: {} - es-define-property@1.0.1: {} - - es-errors@1.3.0: {} - - es-object-atoms@1.1.1: - dependencies: - es-errors: 1.3.0 - esbuild-register@3.6.0(esbuild@0.25.12): dependencies: debug: 4.4.3 @@ -5268,13 +4861,11 @@ snapshots: '@esbuild/win32-ia32': 0.25.12 '@esbuild/win32-x64': 0.25.12 - escape-html@1.0.3: {} - escape-string-regexp@4.0.0: {} escape-string-regexp@5.0.0: {} - eslint-plugin-svelte@3.13.0(eslint@9.39.1(jiti@2.6.1))(svelte@5.44.0): + eslint-plugin-svelte@3.13.1(eslint@9.39.1(jiti@2.6.1))(svelte@5.45.8): dependencies: '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1)) '@jridgewell/sourcemap-codec': 1.5.5 @@ -5286,9 +4877,9 @@ snapshots: postcss-load-config: 3.1.4(postcss@8.5.6) postcss-safe-parser: 7.0.1(postcss@8.5.6) semver: 7.7.3 - svelte-eslint-parser: 1.4.0(svelte@5.44.0) + svelte-eslint-parser: 1.4.1(svelte@5.45.8) optionalDependencies: - svelte: 5.44.0 + svelte: 5.45.8 transitivePeerDependencies: - ts-node @@ -5308,7 +4899,7 @@ snapshots: '@eslint/config-array': 0.21.1 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.1 + '@eslint/eslintrc': 3.3.3 '@eslint/js': 9.39.1 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 @@ -5354,7 +4945,7 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.1.3: + esrap@2.2.1: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -5366,50 +4957,6 @@ snapshots: esutils@2.0.3: {} - etag@1.8.1: {} - - eventsource-parser@3.0.6: {} - - eventsource@3.0.7: - dependencies: - eventsource-parser: 3.0.6 - - express-rate-limit@7.5.1(express@5.1.0): - dependencies: - express: 5.1.0 - - express@5.1.0: - dependencies: - accepts: 2.0.0 - body-parser: 2.2.0 - content-disposition: 1.0.1 - content-type: 1.0.5 - cookie: 0.7.2 - cookie-signature: 1.2.2 - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - finalhandler: 2.1.0 - fresh: 2.0.0 - http-errors: 2.0.1 - merge-descriptors: 2.0.0 - mime-types: 3.0.2 - on-finished: 2.4.1 - once: 1.4.0 - parseurl: 1.3.3 - proxy-addr: 2.0.7 - qs: 6.14.0 - range-parser: 1.2.1 - router: 2.2.0 - send: 1.2.0 - serve-static: 2.2.0 - statuses: 2.0.2 - type-is: 2.0.1 - vary: 1.1.2 - transitivePeerDependencies: - - supports-color - extend@3.0.2: {} fast-check@3.23.2: @@ -5418,26 +4965,12 @@ snapshots: fast-deep-equal@3.1.3: {} - fast-diff@1.3.0: {} - - fast-glob@3.3.3: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} fast-uri@3.1.0: {} - fastq@1.19.1: - dependencies: - reusify: 1.1.0 - fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -5451,21 +4984,6 @@ snapshots: dependencies: flat-cache: 4.0.1 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - finalhandler@2.1.0: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - on-finished: 2.4.1 - parseurl: 1.3.3 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - find-my-way-ts@0.1.6: {} find-up@5.0.0: @@ -5484,44 +5002,16 @@ snapshots: dependencies: fetch-blob: 3.2.0 - forwarded@0.2.0: {} - - fresh@2.0.0: {} - fsevents@2.3.2: optional: true fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - - get-intrinsic@1.3.0: - dependencies: - call-bind-apply-helpers: 1.0.2 - es-define-property: 1.0.1 - es-errors: 1.3.0 - es-object-atoms: 1.1.1 - function-bind: 1.1.2 - get-proto: 1.0.1 - gopd: 1.2.0 - has-symbols: 1.1.0 - hasown: 2.0.2 - math-intrinsics: 1.1.0 - - get-proto@1.0.1: - dependencies: - dunder-proto: 1.0.1 - es-object-atoms: 1.1.1 - get-tsconfig@4.13.0: dependencies: resolve-pkg-maps: 1.0.0 - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 @@ -5530,36 +5020,10 @@ snapshots: globals@16.5.0: {} - gopd@1.2.0: {} - graceful-fs@4.2.11: {} - graphemer@1.4.0: {} - has-flag@4.0.0: {} - has-symbols@1.1.0: {} - - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - - http-errors@2.0.1: - dependencies: - depd: 2.0.0 - inherits: 2.0.4 - setprototypeof: 1.2.0 - statuses: 2.0.2 - toidentifier: 1.0.1 - - iconv-lite@0.6.3: - dependencies: - safer-buffer: 2.1.2 - - iconv-lite@0.7.0: - dependencies: - safer-buffer: 2.1.2 - ignore@5.3.2: {} ignore@7.0.5: {} @@ -5571,22 +5035,14 @@ snapshots: imurmurhash@0.1.4: {} - inherits@2.0.4: {} - - ipaddr.js@1.9.1: {} - is-extglob@2.1.1: {} is-glob@4.0.3: dependencies: is-extglob: 2.1.1 - is-number@7.0.0: {} - is-plain-obj@4.1.0: {} - is-promise@4.0.0: {} - is-reference@3.0.3: dependencies: '@types/estree': 1.0.8 @@ -5609,7 +5065,7 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} - katex@0.16.25: + katex@0.16.27: dependencies: commander: 8.3.0 @@ -5710,7 +5166,15 @@ snapshots: longest-streak@3.1.0: {} - loro-crdt@1.10.0: {} + loro-codemirror@0.3.3(@codemirror/state@6.5.2)(@codemirror/view@6.39.0)(loro-crdt@1.10.3): + dependencies: + '@codemirror/state': 6.5.2 + '@codemirror/view': 6.39.0 + loro-crdt: 1.10.3 + + loro-crdt@1.10.3: {} + + lz-string@1.5.0: {} magic-string@0.30.21: dependencies: @@ -5729,8 +5193,6 @@ snapshots: markdown-table@3.0.4: {} - math-intrinsics@1.1.0: {} - mdast-util-definitions@6.0.0: dependencies: '@types/mdast': 4.0.4 @@ -5841,12 +5303,6 @@ snapshots: mdurl@2.0.0: {} - media-typer@1.1.0: {} - - merge-descriptors@2.0.0: {} - - merge2@1.4.1: {} - micromark-core-commonmark@2.0.3: dependencies: decode-named-character-reference: 1.2.0 @@ -6038,17 +5494,6 @@ snapshots: transitivePeerDependencies: - supports-color - micromatch@4.0.8: - dependencies: - braces: 3.0.3 - picomatch: 2.3.1 - - mime-db@1.54.0: {} - - mime-types@3.0.2: - dependencies: - mime-db: 1.54.0 - minimatch@3.1.2: dependencies: brace-expansion: 1.1.12 @@ -6087,8 +5532,6 @@ snapshots: natural-compare@1.4.0: {} - negotiator@1.0.0: {} - node-domexception@1.0.0: {} node-emoji@2.2.0: @@ -6109,18 +5552,6 @@ snapshots: detect-libc: 2.1.2 optional: true - object-assign@4.1.1: {} - - object-inspect@1.13.4: {} - - on-finished@2.4.1: - dependencies: - ee-first: 1.1.1 - - once@1.4.0: - dependencies: - wrappy: 1.0.2 - optionator@0.9.4: dependencies: deep-is: 0.1.4 @@ -6144,27 +5575,19 @@ snapshots: dependencies: callsites: 3.1.0 - parseurl@1.3.3: {} - path-exists@4.0.0: {} path-key@3.1.1: {} - path-to-regexp@8.3.0: {} - picocolors@1.1.1: {} - picomatch@2.3.1: {} - picomatch@4.0.3: {} - pkce-challenge@5.0.0: {} - - playwright-core@1.56.1: {} + playwright-core@1.57.0: {} - playwright@1.56.1: + playwright@1.57.0: dependencies: - playwright-core: 1.56.1 + playwright-core: 1.57.0 optionalDependencies: fsevents: 2.3.2 @@ -6188,7 +5611,7 @@ snapshots: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@7.1.0: + postcss-selector-parser@7.1.1: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -6201,18 +5624,18 @@ snapshots: prelude-ls@1.2.1: {} - prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0): + prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.45.8): dependencies: - prettier: 3.6.2 - svelte: 5.44.0 + prettier: 3.7.4 + svelte: 5.45.8 - prettier-plugin-tailwindcss@0.7.1(prettier-plugin-svelte@3.4.0(prettier@3.6.2)(svelte@5.44.0))(prettier@3.6.2): + prettier-plugin-tailwindcss@0.7.2(prettier-plugin-svelte@3.4.0(prettier@3.7.4)(svelte@5.45.8))(prettier@3.7.4): dependencies: - prettier: 3.6.2 + prettier: 3.7.4 optionalDependencies: - prettier-plugin-svelte: 3.4.0(prettier@3.6.2)(svelte@5.44.0) + prettier-plugin-svelte: 3.4.0(prettier@3.7.4)(svelte@5.45.8) - prettier@3.6.2: {} + prettier@3.7.4: {} promise-limit@2.7.0: {} @@ -6234,20 +5657,20 @@ snapshots: dependencies: prosemirror-state: 1.4.4 prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.3 + prosemirror-view: 1.41.4 prosemirror-gapcursor@1.4.0: dependencies: prosemirror-keymap: 1.2.3 prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 - prosemirror-view: 1.41.3 + prosemirror-view: 1.41.4 prosemirror-history@1.5.0: dependencies: prosemirror-state: 1.4.4 prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.3 + prosemirror-view: 1.41.4 rope-sequence: 1.3.4 prosemirror-inputrules@1.5.1: @@ -6280,7 +5703,7 @@ snapshots: prosemirror-safari-ime-span@1.0.2: dependencies: prosemirror-state: 1.4.4 - prosemirror-view: 1.41.3 + prosemirror-view: 1.41.4 prosemirror-schema-basic@1.2.4: dependencies: @@ -6296,62 +5719,42 @@ snapshots: dependencies: prosemirror-model: 1.25.4 prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.3 + prosemirror-view: 1.41.4 - prosemirror-tables@1.8.1: + prosemirror-tables@1.8.3: dependencies: prosemirror-keymap: 1.2.3 prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 prosemirror-transform: 1.10.5 - prosemirror-view: 1.41.3 + prosemirror-view: 1.41.4 - prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.3): + prosemirror-trailing-node@3.0.0(prosemirror-model@1.25.4)(prosemirror-state@1.4.4)(prosemirror-view@1.41.4): dependencies: '@remirror/core-constants': 3.0.0 escape-string-regexp: 4.0.0 prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 - prosemirror-view: 1.41.3 + prosemirror-view: 1.41.4 prosemirror-transform@1.10.5: dependencies: prosemirror-model: 1.25.4 - prosemirror-view@1.41.3: + prosemirror-view@1.41.4: dependencies: prosemirror-model: 1.25.4 prosemirror-state: 1.4.4 prosemirror-transform: 1.10.5 - proxy-addr@2.0.7: - dependencies: - forwarded: 0.2.0 - ipaddr.js: 1.9.1 - punycode.js@2.3.1: {} punycode@2.3.1: {} pure-rand@6.1.0: {} - qs@6.14.0: - dependencies: - side-channel: 1.1.0 - - queue-microtask@1.2.3: {} - raf-schd@4.0.3: {} - range-parser@1.2.1: {} - - raw-body@3.0.2: - dependencies: - bytes: 3.1.2 - http-errors: 2.0.1 - iconv-lite: 0.7.0 - unpipe: 1.0.0 - readdirp@4.1.2: {} remark-gfm@4.0.1: @@ -6401,8 +5804,6 @@ snapshots: resolve-pkg-maps@1.0.0: {} - reusify@1.1.0: {} - rollup@4.53.3: dependencies: '@types/estree': 1.0.8 @@ -6433,93 +5834,32 @@ snapshots: rope-sequence@1.3.4: {} - router@2.2.0: - dependencies: - debug: 4.4.3 - depd: 2.0.0 - is-promise: 4.0.0 - parseurl: 1.3.3 - path-to-regexp: 8.3.0 - transitivePeerDependencies: - - supports-color - - run-parallel@1.2.0: + runed@0.37.0(@sveltejs/kit@2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(zod@4.1.13): dependencies: - queue-microtask: 1.2.3 + dequal: 2.0.3 + esm-env: 1.2.2 + lz-string: 1.5.0 + svelte: 5.45.8 + optionalDependencies: + '@sveltejs/kit': 2.49.2(@sveltejs/vite-plugin-svelte@6.2.1(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)))(svelte@5.45.8)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)) + zod: 4.1.13 sade@1.8.1: dependencies: mri: 1.2.0 - safer-buffer@2.1.2: {} - scule@1.3.0: {} semver@7.7.3: {} - send@1.2.0: - dependencies: - debug: 4.4.3 - encodeurl: 2.0.0 - escape-html: 1.0.3 - etag: 1.8.1 - fresh: 2.0.0 - http-errors: 2.0.1 - mime-types: 3.0.2 - ms: 2.1.3 - on-finished: 2.4.1 - range-parser: 1.2.1 - statuses: 2.0.2 - transitivePeerDependencies: - - supports-color - - serve-static@2.2.0: - dependencies: - encodeurl: 2.0.0 - escape-html: 1.0.3 - parseurl: 1.3.3 - send: 1.2.0 - transitivePeerDependencies: - - supports-color - set-cookie-parser@2.7.2: {} - setprototypeof@1.2.0: {} - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} - side-channel-list@1.0.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - - side-channel-map@1.0.1: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - - side-channel-weakmap@1.0.2: - dependencies: - call-bound: 1.0.4 - es-errors: 1.3.0 - get-intrinsic: 1.3.0 - object-inspect: 1.13.4 - side-channel-map: 1.0.1 - - side-channel@1.1.0: - dependencies: - es-errors: 1.3.0 - object-inspect: 1.13.4 - side-channel-list: 1.0.0 - side-channel-map: 1.0.1 - side-channel-weakmap: 1.0.2 - sirv@3.0.2: dependencies: '@polka/url': 1.0.0-next.29 @@ -6539,8 +5879,6 @@ snapshots: source-map@0.6.1: {} - statuses@2.0.2: {} - strip-json-comments@3.1.1: {} style-mod@4.1.3: {} @@ -6549,49 +5887,49 @@ snapshots: dependencies: has-flag: 4.0.0 - svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.44.0)(typescript@5.9.3): + svelte-check@4.3.4(picomatch@4.0.3)(svelte@5.45.8)(typescript@5.9.3): dependencies: '@jridgewell/trace-mapping': 0.3.31 chokidar: 4.0.3 fdir: 6.5.0(picomatch@4.0.3) picocolors: 1.1.1 sade: 1.8.1 - svelte: 5.44.0 + svelte: 5.45.8 typescript: 5.9.3 transitivePeerDependencies: - picomatch - svelte-eslint-parser@1.4.0(svelte@5.44.0): + svelte-eslint-parser@1.4.1(svelte@5.45.8): dependencies: eslint-scope: 8.4.0 eslint-visitor-keys: 4.2.1 espree: 10.4.0 postcss: 8.5.6 postcss-scss: 4.0.9(postcss@8.5.6) - postcss-selector-parser: 7.1.0 + postcss-selector-parser: 7.1.1 optionalDependencies: - svelte: 5.44.0 + svelte: 5.45.8 - svelte2tsx@0.7.45(svelte@5.44.0)(typescript@5.9.3): + svelte2tsx@0.7.45(svelte@5.45.8)(typescript@5.9.3): dependencies: dedent-js: 1.0.1 scule: 1.3.0 - svelte: 5.44.0 + svelte: 5.45.8 typescript: 5.9.3 - svelte@5.44.0: + svelte@5.45.8: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 - '@sveltejs/acorn-typescript': 1.0.7(acorn@8.15.0) + '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) '@types/estree': 1.0.8 acorn: 8.15.0 aria-query: 5.3.2 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.5.0 + devalue: 5.6.0 esm-env: 1.2.2 - esrap: 2.1.3 + esrap: 2.2.1 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -6601,25 +5939,25 @@ snapshots: tapable@2.3.0: {} + temporal-polyfill@0.3.0: + dependencies: + temporal-spec: 0.3.0 + + temporal-spec@0.3.0: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tiptap-markdown@0.9.0(@tiptap/core@3.11.0(@tiptap/pm@3.11.0)): + tiptap-markdown@0.9.0(@tiptap/core@3.13.0(@tiptap/pm@3.13.0)): dependencies: - '@tiptap/core': 3.11.0(@tiptap/pm@3.11.0) + '@tiptap/core': 3.13.0(@tiptap/pm@3.13.0) '@types/markdown-it': 13.0.9 markdown-it: 14.1.0 markdown-it-task-lists: 2.1.1 prosemirror-markdown: 1.13.2 - to-regex-range@5.0.1: - dependencies: - is-number: 7.0.0 - - toidentifier@1.0.1: {} - totalist@3.0.1: {} trough@2.2.0: {} @@ -6639,27 +5977,21 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-is@2.0.1: + typescript-eslint@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): dependencies: - content-type: 1.0.5 - media-typer: 1.1.0 - mime-types: 3.0.2 - - typescript-eslint@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3): - dependencies: - '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) - '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.49.0(@typescript-eslint/parser@8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/parser': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) + '@typescript-eslint/typescript-estree': 8.49.0(typescript@5.9.3) + '@typescript-eslint/utils': 8.49.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3) eslint: 9.39.1(jiti@2.6.1) typescript: 5.9.3 transitivePeerDependencies: - supports-color - typescript-svelte-plugin@0.3.50(svelte@5.44.0)(typescript@5.9.3): + typescript-svelte-plugin@0.3.50(svelte@5.45.8)(typescript@5.9.3): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - svelte2tsx: 0.7.45(svelte@5.44.0)(typescript@5.9.3) + svelte2tsx: 0.7.45(svelte@5.45.8)(typescript@5.9.3) transitivePeerDependencies: - svelte - typescript @@ -6701,8 +6033,6 @@ snapshots: unist-util-is: 6.0.1 unist-util-visit-parents: 6.0.2 - unpipe@1.0.0: {} - uri-js@4.4.1: dependencies: punycode: 2.3.1 @@ -6713,8 +6043,6 @@ snapshots: uuid@11.1.0: {} - vary@1.1.2: {} - vfile-message@4.0.3: dependencies: '@types/unist': 3.0.3 @@ -6725,27 +6053,27 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plugin-devtools-json@1.0.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)): + vite-plugin-devtools-json@1.0.0(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)): dependencies: uuid: 11.1.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) - vite-plugin-top-level-await@1.6.0(rollup@4.53.3)(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)): + vite-plugin-top-level-await@1.6.0(rollup@4.53.3)(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)): dependencies: '@rollup/plugin-virtual': 3.0.2(rollup@4.53.3) '@swc/core': 1.15.3 '@swc/wasm': 1.15.3 uuid: 10.0.0 - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) transitivePeerDependencies: - '@swc/helpers' - rollup - vite-plugin-wasm@3.5.0(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)): + vite-plugin-wasm@3.5.0(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)): dependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) - vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1): + vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.3) @@ -6758,11 +6086,10 @@ snapshots: fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 - yaml: 2.8.1 - vitefu@1.1.1(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1)): + vitefu@1.1.1(vite@7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)): optionalDependencies: - vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)(yaml@2.8.1) + vite: 7.2.7(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2) w3c-keyname@2.2.8: {} @@ -6774,23 +6101,15 @@ snapshots: word-wrap@1.2.5: {} - wrappy@1.0.2: {} - ws@8.18.3: {} yaml@1.10.2: {} - yaml@2.8.1: - optional: true - yocto-queue@0.1.0: {} zimmerframe@1.1.4: {} - zod-to-json-schema@3.25.0(zod@3.25.76): - dependencies: - zod: 3.25.76 - - zod@3.25.76: {} + zod@4.1.13: + optional: true zwitch@2.0.4: {} diff --git a/scripts/check_db.ts b/scripts/check_db.ts new file mode 100644 index 0000000..2febdc3 --- /dev/null +++ b/scripts/check_db.ts @@ -0,0 +1,29 @@ +import "dotenv/config"; + +import { sql, count } from "drizzle-orm"; +import { users } from "$lib/server/db/schema.ts"; +import { createClient } from "@libsql/client"; +import { drizzle } from "drizzle-orm/libsql"; +import * as schema from "../src/lib/server/db/schema.ts"; + +if (!process.env["DATABASE_URL"]) throw new Error("DATABASE_URL is not set"); + +const client = createClient({ + url: process.env["DATABASE_URL"], +}); + +const db = drizzle(client, { schema }); + +try { + const result = await db.run(sql`PRAGMA tabdrizzle, le_info(users);`); + console.log("Users table columns:"); + result.rows.forEach((row) => { + const r = row as unknown as { name: string; type: string }; + console.log(`- ${r.name} (${r.type})`); + }); + + const [userCount] = await db.select({ count: count() }).from(users); + console.log(`User count: ${String(userCount?.count ?? 0)}`); +} catch (e) { + console.error("Error:", e); +} diff --git a/scripts/check_identity.ts b/scripts/check_identity.ts new file mode 100644 index 0000000..af5d605 --- /dev/null +++ b/scripts/check_identity.ts @@ -0,0 +1,24 @@ +import { fetchUserIdentity } from "../src/lib/server/federation.ts"; + +console.log("=== Identity Fetch Verification ==="); +const handle = "@bob:localhost:5174"; +const requestingDomain = "localhost:5173"; + +try { + console.log(`Fetching identity for ${handle} from ${requestingDomain}...`); + const identity = await fetchUserIdentity(handle, requestingDomain); + + console.log("Result:"); + console.log(JSON.stringify(identity, null, 2)); + + if (identity?.publicKey) { + console.log( + "\nPublic Key First 10 chars:", + identity.publicKey.slice(0, 10), + ); + } else { + console.log("\n❌ No Public Key found!"); + } +} catch (e) { + console.error("❌ Error fetching identity:", e); +} diff --git a/scripts/check_keypair.ts b/scripts/check_keypair.ts new file mode 100644 index 0000000..e215985 --- /dev/null +++ b/scripts/check_keypair.ts @@ -0,0 +1,65 @@ +import "dotenv/config"; +import { users } from "../src/lib/server/db/schema.ts"; +import { eq } from "drizzle-orm"; +import { + encryptKeyForDevice, + decryptKeyForDevice, + generateNoteKey, +} from "../src/lib/crypto.ts"; +import { createClient } from "@libsql/client"; +import { drizzle } from "drizzle-orm/libsql"; +import * as schema from "../src/lib/server/db/schema.ts"; + +if (!process.env["DATABASE_URL"]) throw new Error("DATABASE_URL is not set"); + +const client = createClient({ + url: process.env["DATABASE_URL"], +}); + +const db = drizzle(client, { schema }); + +console.log("=== Keypair ConsiscreateClient, tency Check ==="); + +// 1. Get Bob's keys +const bob = await db.query.users.findFirst({ + where: eq(users.username, "bob"), +}); + +if (!bob) { + console.error("❌ Bob not found in DB!"); + process.exit(1); +} + +console.log("Bob:", bob.id); +console.log("Public Key: ", bob.publicKey); +console.log("Private Key:", bob.privateKeyEncrypted); + +if (!bob.publicKey || !bob.privateKeyEncrypted) { + console.error("❌ Missing keys!"); + process.exit(1); +} + +// 2. Test Keypair +const secret = generateNoteKey(); +console.log("\nTest Secret:", secret); + +try { + // Encrypt to Bob's Public Key + const envelope = await encryptKeyForDevice(secret, bob.publicKey); + console.log("Encrypted Envelope:", envelope); + + // Decrypt with Bob's Private Key + const decrypted = await decryptKeyForDevice( + envelope, + bob.privateKeyEncrypted, + ); + console.log("Decrypted Secret: ", decrypted); + + if (decrypted === secret) { + console.log("\n✅ SUCCESS: Stored keys are a valid pair!"); + } else { + console.error("\n❌ FAILURE: Decrypted secret does not match!"); + } +} catch (e) { + console.error("\n❌ CRITICAL ERROR during test:", e); +} diff --git a/scripts/logout.ts b/scripts/logout.ts new file mode 100644 index 0000000..ae63816 --- /dev/null +++ b/scripts/logout.ts @@ -0,0 +1,22 @@ +import "dotenv/config"; +import { createClient } from "@libsql/client"; +import { drizzle } from "drizzle-orm/libsql"; +import * as schema from "../src/lib/server/db/schema.ts"; + +if (!process.env["DATABASE_URL"]) throw new Error("DATABASE_URL is not set"); + +const client = createClient({ + url: process.env["DATABASE_URL"], +}); + +const db = drizzle(client, { schema }); + +console.log("Clearing all sessions..."); +try { + await db.delete(schema.sessions); + console.log("Successfully deleted all sessions."); +} catch (error) { + console.error("Error clearing sessions:", error); + process.exit(1); +} +process.exit(0); diff --git a/scripts/reproduce_crypto.ts b/scripts/reproduce_crypto.ts new file mode 100644 index 0000000..f8a2b34 --- /dev/null +++ b/scripts/reproduce_crypto.ts @@ -0,0 +1,43 @@ +import { + generateEncryptionKeyPair, + encryptKeyForDevice, + decryptKeyForDevice, + generateNoteKey, +} from "../src/lib/crypto.ts"; + +console.log("=== Crypto Verification Start ==="); + +// 1. Generate Identity (User B) +console.log("1. Generating User B keys..."); +const userB = await generateEncryptionKeyPair(); +console.log(" User B Public: ", userB.publicKey); +console.log(" User B Private:", userB.privateKey); + +// 2. Generate Note Key (Server A) +console.log("\n2. Generating Note Key..."); +const noteKey = generateNoteKey(); +console.log(" Note Key (Original):", noteKey); +console.log(" Note Key Length: ", noteKey.length); + +// 3. Encrypt for User B (Server A action) +console.log("\n3. Encrypting for User B..."); +try { + const envelope = await encryptKeyForDevice(noteKey, userB.publicKey); + console.log(" Envelope: ", envelope); + console.log(" Envelope Length:", envelope.length); + + // 4. Decrypt as User B (Client B action) + console.log("\n4. Decrypting as User B..."); + const decryptedKey = await decryptKeyForDevice(envelope, userB.privateKey); + console.log(" Decrypted Key: ", decryptedKey); + + if (decryptedKey === noteKey) { + console.log("\n✅ SUCCESS: Keys match!"); + } else { + console.error("\n❌ FAILURE: Keys do not match!"); + console.error("Expected:", noteKey); + console.error("Got: ", decryptedKey); + } +} catch (e) { + console.error("\n❌ CRITICAL ERROR:", e); +} diff --git a/scripts/start_federation.sh b/scripts/start_federation.sh new file mode 100755 index 0000000..8a51611 --- /dev/null +++ b/scripts/start_federation.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Function to kill all child processes on exit +cleanup() { + echo "Stopping servers..." + # Kill all child processes in the same process group + kill 0 +} + +# Trap SIGINT (Ctrl+C) and call cleanup +trap cleanup SIGINT SIGTERM EXIT + +# Start Server A (Alice) +echo "Migrating Alice's DB..." +DATABASE_URL=file:local-a.db pnpm db:push + +echo "Starting Alice (Server A) on port 5173..." +SERVER_DOMAIN=localhost:5173 DATABASE_URL=file:local-a.db ORIGIN=http://localhost:5173 pnpm run dev --port 5173 & + +# Start Server B (Bob) +echo "Migrating Bob's DB..." +DATABASE_URL=file:local-b.db pnpm db:push + +echo "Starting Bob (Server B) on port 5174..." +SERVER_DOMAIN=localhost:5174 DATABASE_URL=file:local-b.db ORIGIN=http://localhost:5174 pnpm run dev --port 5174 & + +# Wait for both processes to keep the script running +wait diff --git a/scripts/test-crypto.ts b/scripts/test-crypto.ts new file mode 100644 index 0000000..8486126 --- /dev/null +++ b/scripts/test-crypto.ts @@ -0,0 +1,60 @@ +import { pbkdf2 } from "@noble/hashes/pbkdf2.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { xchacha20poly1305 } from "@noble/ciphers/chacha.js"; + +function encryptWithPassword( + dataBase64: string, + password: string, +): Uint8Array { + const data = Uint8Array.from(dataBase64); + const salt = crypto.getRandomValues(new Uint8Array(16)); + // Derive key from password + const kek = pbkdf2(sha256, password, salt, { c: 600000, dkLen: 32 }); + + // Encrypt + const nonce = crypto.getRandomValues(new Uint8Array(24)); + const chacha = xchacha20poly1305(kek, nonce); + const ciphertext = chacha.encrypt(data); + + // Pack: salt + nonce + ciphertext + const result = new Uint8Array(16 + 24 + ciphertext.length); + result.set(salt, 0); + result.set(nonce, 16); + result.set(ciphertext, 40); + + return Uint8Array.from(result); +} + +function decryptWithPassword( + encryptedBase64: Uint8Array, + password: string, +): Uint8Array { + const salt = encryptedBase64.slice(0, 16); + const nonce = encryptedBase64.slice(16, 40); + const ciphertext = encryptedBase64.slice(40); + + const kek = pbkdf2(sha256, password, salt, { c: 600000, dkLen: 32 }); + const chacha = xchacha20poly1305(kek, nonce); + const data = chacha.decrypt(ciphertext); + + return Uint8Array.from(data); +} + +const rawData = "This is a secret key"; +const rawBase64 = new TextEncoder().encode(rawData).toBase64(); +const pass = "password123"; + +console.log("Original:", rawData); +const encrypted = encryptWithPassword(rawBase64, pass); +console.log("Encrypted:", encrypted); + +const decryptedBase64 = decryptWithPassword(encrypted, pass); +const decrypted = new TextDecoder().decode(decryptedBase64); +console.log("Decrypted:", decrypted); + +if (rawData === decrypted) { + console.log("PASS"); +} else { + console.error("FAIL"); + process.exit(1); +} diff --git a/scripts/verify_alice_key.ts b/scripts/verify_alice_key.ts new file mode 100644 index 0000000..7698325 --- /dev/null +++ b/scripts/verify_alice_key.ts @@ -0,0 +1,15 @@ +import { decryptKeyForDevice } from "../src/lib/crypto.ts"; + +const alicePriv = "8yxKreQsh2I8gQiL4GsHQQs4LSJGlOVVDETkIU6NB2c="; +const noteKeyEnc = + "c1i1h1NO8WN3buoPWrmY++GQFulEtokhhNnJshR/ygNwiNo96p/spl3lJKJt42D6LeeaBEBjdM1Ioq3iJ3kzLXhKX8kNYOll1KMkAJCoUiRbr6HG2+DCEd1xeT6fixowjKc8BCXeTfc="; + +console.log("=== Verifying Alice's Decryption ==="); +try { + const raw = await decryptKeyForDevice(noteKeyEnc, alicePriv); + console.log("✅ SUCCESS!"); + console.log("Raw Key:", raw); + console.log("Length:", raw.length); // Should be 44 +} catch (e) { + console.error("❌ FAILED:", e); +} diff --git a/scripts/verify_keys.ts b/scripts/verify_keys.ts new file mode 100644 index 0000000..ec5f406 --- /dev/null +++ b/scripts/verify_keys.ts @@ -0,0 +1,31 @@ +import { + encryptKeyForDevice, + decryptKeyForDevice, + generateNoteKey, +} from "../src/lib/crypto.ts"; + +// I will replace these with values from the DB +const publicKey = "ouOeCZu2NN+erXNttehhxtnIBwdFgkANhxkJtrwqNCg="; +const privateKey = "H/UgylrpmcHHpYuhanuLDUOd/VAzWouh75xyEXUxLh8="; + +if (publicKey.includes("REPLACE")) { + console.error("Please replace placeholder keys!"); + process.exit(1); +} + +console.log("=== Verifying Keys ==="); +console.log("Pub:", publicKey.slice(0, 10) + "..."); + +const secret = generateNoteKey(); +try { + const envelope = await encryptKeyForDevice(secret, publicKey); + const decrypted = await decryptKeyForDevice(envelope, privateKey); + + if (decrypted === secret) { + console.log("✅ SUCCESS: Keys work!"); + } else { + console.error("❌ FAILURE: Decryption mismatch"); + } +} catch (e) { + console.error("❌ ERROR:", e); +} diff --git a/server-a-identity.json b/server-a-identity.json new file mode 100644 index 0000000..4a6294c --- /dev/null +++ b/server-a-identity.json @@ -0,0 +1,5 @@ +{ + "publicKey": "lIOFN8n4HkE5DXjzFsa+xYL9CtFTedbe7/rQR0Kr0uA=", + "privateKey": "wRgtdxuggx7rIlRbe4+A1W46bQ2AB8LZX5PIyshVM/w=", + "domain": "localhost:5173" +} diff --git a/server-b-identity.json b/server-b-identity.json new file mode 100644 index 0000000..2f802bd --- /dev/null +++ b/server-b-identity.json @@ -0,0 +1,5 @@ +{ + "publicKey": "xG1+klHbBRobqxsz8oJ2ty2Km4hMHzd+y8A6Btw5E1k=", + "privateKey": "UCzPB9k/xfFFYuJVPmY2Za1Jvuw8o7E4FnH+PBwyLJc=", + "domain": "localhost:5174" +} diff --git a/server-identity.json b/server-identity.json new file mode 100644 index 0000000..5e88553 --- /dev/null +++ b/server-identity.json @@ -0,0 +1,7 @@ +{ + "publicKey": "0a3VH8qgnCjcKQ0PzHm3gqAWS5TdkD9YgePtQcSo+wo=", + "privateKey": "5JLwAahm/nzLwpoUjW6ykPQrRDajDjSpEC4CWn06DRo=", + "domain": "localhost:5174", + "encryptionPublicKey": "YTrup6VwXOzS1hDP/DBzr7YwW5IFlvN0mNtz01Bvgyo=", + "encryptionPrivateKey": "W6IS4SujT5QLmoKJ23KHLrKlGxxdkgDz/gjpm6g20aI=" +} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index 7ce13ee..346bd8f 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -9,7 +9,14 @@ const handleAuth: Handle = async ({ event, resolve }) => { event.locals.session = undefined; // Redirect to login if accessing protected routes - if (!event.route.id?.startsWith("/(auth)")) { + if ( + !event.route.id?.startsWith("/(auth)") && + !event.route.id?.startsWith("/federation") && + !event.route.id?.startsWith("/.well-known") && + !event.route.id?.startsWith("/notes") && + !event.route.id?.startsWith("/client") && + !event.route.id?.startsWith("/api") + ) { return new Response("Redirect", { status: 303, headers: { Location: "/login" }, diff --git a/src/lib/components/ConfirmationModal.svelte b/src/lib/components/ConfirmationModal.svelte new file mode 100644 index 0000000..653840e --- /dev/null +++ b/src/lib/components/ConfirmationModal.svelte @@ -0,0 +1,99 @@ + + +{#if isOpen} + +{/if} diff --git a/src/lib/components/HistoryPanel.svelte b/src/lib/components/HistoryPanel.svelte new file mode 100644 index 0000000..f8dcdff --- /dev/null +++ b/src/lib/components/HistoryPanel.svelte @@ -0,0 +1,213 @@ + + + { + isOpen = false; + }} +> +
+ +
+
+ +

Version History

+
+ +
+ + +
+ {#if history.length === 0} +
+
+ +

No history available

+
+
+ {:else} +
+ {#each history as entry, i (entry.version)} + + {/each} +
+ {/if} +
+ + + {#if selectedVersion !== null && selectedVersion !== history[0]?.version} + {@const cachedVersion = selectedVersion} +
+ +
+ {/if} +
+ +
+ + + + + diff --git a/src/lib/components/MemberAvatars.svelte b/src/lib/components/MemberAvatars.svelte new file mode 100644 index 0000000..c10d4f5 --- /dev/null +++ b/src/lib/components/MemberAvatars.svelte @@ -0,0 +1,75 @@ + + +
+ {#each visibleMembers as member (member.userId)} +
+ {getInitial(member.userId)} +
+ {/each} + + {#if remainingCount > 0} +
+ +{remainingCount} +
+ {/if} +
diff --git a/src/lib/components/MembersModal.svelte b/src/lib/components/MembersModal.svelte new file mode 100644 index 0000000..57b3b4c --- /dev/null +++ b/src/lib/components/MembersModal.svelte @@ -0,0 +1,198 @@ + + +{#if isOpen} +
+
e.stopPropagation()} + > + +
+

Members of "{noteTitle}"

+ +
+ + +
+ {#if loading} +
+ +
+ {:else if error} +
+ {error} +
+ {:else if members.length === 0} +
+ No members found +
+ {:else} +
+ {#each members as member (member.userId)} + {@const RoleIcon = getRoleIcon(member.role)} +
+
+
+ {member.userId.charAt(0).toUpperCase()} +
+
+
+ {member.userId} +
+
+ + {formatRole(member.role)} +
+
+
+ + {#if isOwner && member.role !== "owner"} + + {/if} +
+ {/each} +
+ {/if} +
+ + +
+ +
+
+
+{/if} + + (memberToRemoveId = null)} +/> diff --git a/src/lib/components/ProfilePicture.svelte b/src/lib/components/ProfilePicture.svelte index 4237a18..4094981 100644 --- a/src/lib/components/ProfilePicture.svelte +++ b/src/lib/components/ProfilePicture.svelte @@ -7,7 +7,7 @@
- {name[0]?.toUpperCase()} + {name.toUpperCase()}
diff --git a/src/lib/components/ShareModal.svelte b/src/lib/components/ShareModal.svelte new file mode 100644 index 0000000..9d6c4b4 --- /dev/null +++ b/src/lib/components/ShareModal.svelte @@ -0,0 +1,430 @@ + + +{#if isOpen} +
+
e.stopPropagation()} + > + +
+
+ +

Share "{noteTitle}"

+
+ +
+ + +
+ +
+ + + + + + + + + + + +
+ + + {#if accessLevel === "password_protected"} +
+ + +

+ Note: Because we use End-to-End Encryption, if you lose this + password, nobody (including the server) can recover the access for + others. +

+
+ {/if} + + + {#if accessLevel === "invite_only"} +
+ + +
+ + e.key === "Enter" && addInvitedUser()} + /> + +
+ + {#if invitedUsers.length > 0} +
+ {#each invitedUsers as user (user)} +
+ {user} + +
+ {/each} +
+ {/if} +
+ {/if} + + + {#if accessLevel !== "private"} +
+ +
+ + + +
+
+ {/if} +
+ + + {#if error} +
+ {error} +
+ {/if} + + +
+ + +
+
+
+{/if} diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index ff2b721..d25efc7 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -1,5 +1,4 @@ -
- +
+ + + + + - n.id === noteId)?.encryptedKey} + isOpen={isShareOpen} + onClose={() => (isShareOpen = false)} />
diff --git a/src/lib/components/codemirror/Toolbar.svelte b/src/lib/components/codemirror/Toolbar.svelte index 918ac3f..4f06078 100644 --- a/src/lib/components/codemirror/Toolbar.svelte +++ b/src/lib/components/codemirror/Toolbar.svelte @@ -1,5 +1,10 @@ -
- {#each tools as toolset, index (index)} -
- {#each toolset as tool (tool.title)} +
+ + {#if sidebarCtx.isCollapsed} + +
+ {/if} + + + {#each sortedGroups as group, i (group.label ?? group.priority)} + {@const isLast = i === sortedGroups.length - 1} + {@const priorityClass = + group.priority === 1 + ? "" + : group.priority === 2 + ? "hidden @md:flex" + : "hidden @lg:flex"} +
+ {#each group.tools as tool (tool.title)} {@const Icon = tool.icon} - {/each}
-
+ {#if !isLast} +
+ {/if} {/each} + + +
diff --git a/src/lib/components/sidebar-context.ts b/src/lib/components/sidebar-context.ts new file mode 100644 index 0000000..f874446 --- /dev/null +++ b/src/lib/components/sidebar-context.ts @@ -0,0 +1,9 @@ +import { createContext } from "svelte"; + +export interface SidebarContext { + isCollapsed: boolean; + toggleSidebar: () => void; +} + +export const [getSidebarContext, setSidebarContext] = + createContext(); diff --git a/src/lib/crypto.ts b/src/lib/crypto.ts index 6cf6fbd..16c1121 100644 --- a/src/lib/crypto.ts +++ b/src/lib/crypto.ts @@ -1,175 +1,304 @@ -/** - * WebCrypto utilities for E2EE - */ +import { Chacha20Poly1305 } from "@hpke/chacha20poly1305"; +import { CipherSuite, DhkemX25519HkdfSha256, HkdfSha256 } from "@hpke/core"; + +const suite = new CipherSuite({ + kem: new DhkemX25519HkdfSha256(), + kdf: new HkdfSha256(), + aead: new Chacha20Poly1305(), +}); -interface KeyPair { - publicKey: string; - privateKey: string; +// ---------------------------------------------------------------------------- +// Types +// ---------------------------------------------------------------------------- + +export interface KeyPair { + publicKey: string; // Base64 + privateKey: string; // Base64 } -export async function generateUserKeys(): Promise { - const keyPair = await crypto.subtle.generateKey( - { - name: "RSA-OAEP", - modulusLength: 2048, - publicExponent: new Uint8Array([1, 0, 1]), - hash: "SHA-256", - }, - true, - ["encrypt", "decrypt"], - ); +export interface DeviceKeys { + signing: KeyPair; + encryption: KeyPair; +} - const publicKeyData = await crypto.subtle.exportKey( - "spki", - keyPair.publicKey, - ); - const privateKeyData = await crypto.subtle.exportKey( +// ---------------------------------------------------------------------------- +// Identity / Signing (Ed25519) +// ---------------------------------------------------------------------------- + +export async function generateSigningKeyPair(): Promise { + const keyPair = await crypto.subtle.generateKey("Ed25519", true, [ + "sign", + "verify", + ]); + + const pub = await crypto.subtle.exportKey("raw", keyPair.publicKey); + const priv = await crypto.subtle.exportKey("pkcs8", keyPair.privateKey); + + return { + publicKey: new Uint8Array(pub).toBase64(), + privateKey: new Uint8Array(priv).toBase64(), + }; +} + +export async function sign( + message: Uint8Array, + privateKeyBase64: string, +): Promise { + const privBytes = Uint8Array.fromBase64(privateKeyBase64); + const privKey = await crypto.subtle.importKey( "pkcs8", - keyPair.privateKey, + privBytes, + "Ed25519", + false, + ["sign"], ); + const signature = await crypto.subtle.sign("Ed25519", privKey, message); + return new Uint8Array(signature).toBase64(); +} - return { - publicKey: new Uint8Array(publicKeyData).toBase64(), +export async function verify( + signatureBase64: string, + message: Uint8Array, + publicKeyBase64: string, +): Promise { + const sigBytes = Uint8Array.fromBase64(signatureBase64); + const pubBytes = Uint8Array.fromBase64(publicKeyBase64); + const pubKey = await crypto.subtle.importKey( + "raw", + pubBytes, + "Ed25519", + false, + ["verify"], + ); + return crypto.subtle.verify("Ed25519", pubKey, sigBytes, message); +} + +// ---------------------------------------------------------------------------- +// Key Exchange / Encryption (HPKE: X25519 + HKDF + ChaCha20Poly1305) +// ---------------------------------------------------------------------------- - // TODO: Proper encryption - // For now, encode private key to base64 - // In production, use PBKDF2 to derive encryption key - privateKey: new Uint8Array(privateKeyData).toBase64(), +export async function generateEncryptionKeyPair(): Promise { + const keyPair = await suite.kem.generateKeyPair(); + const pub = await suite.kem.serializePublicKey(keyPair.publicKey); + const priv = await suite.kem.serializePrivateKey(keyPair.privateKey); + + return { + publicKey: new Uint8Array(pub).toBase64(), + privateKey: new Uint8Array(priv).toBase64(), }; } -export async function generateNoteKey(): Promise { - const key = await crypto.subtle.generateKey( - { - name: "AES-GCM", - length: 256, - }, +// Generate a random 32-byte key for the document +export function generateNoteKey(): string { + const key = globalThis.crypto.getRandomValues(new Uint8Array(32)); + return key.toBase64(); +} + +/** + * Encrypts the document key for a specific recipient device using HPKE. + * Format: [enc (32)] + [ciphertext] + */ +export async function encryptKeyForDevice( + noteKeyBase64: string, + recipientPublicKeyBase64: string, +): Promise { + const noteKey = Uint8Array.fromBase64(noteKeyBase64); + const recipientPub = Uint8Array.fromBase64(recipientPublicKeyBase64); + + const recipientPublicKey = await suite.kem.importKey( + "raw", + recipientPub.buffer, true, - ["encrypt", "decrypt"], ); - const keyData = await crypto.subtle.exportKey("raw", key); - return new Uint8Array(keyData).toBase64(); + const { ct, enc } = await suite.seal({ recipientPublicKey }, noteKey.buffer); + + // Pack: enc (32) + ct + const result = new Uint8Array(enc.byteLength + ct.byteLength); + result.set(new Uint8Array(enc), 0); + result.set(new Uint8Array(ct), enc.byteLength); + + return result.toBase64(); } -export async function encryptKeyForUser( - noteKey: string, - recipientPublicKey: string, +export async function decryptKeyForDevice( + encryptedEnvelopeBase64: string, + devicePrivateKeyBase64: string, ): Promise { - const keyBuffer = Uint8Array.fromBase64(noteKey); - const publicKeyBuffer = Uint8Array.fromBase64(recipientPublicKey); + const envelope = Uint8Array.fromBase64(encryptedEnvelopeBase64); + const devicePriv = Uint8Array.fromBase64(devicePrivateKeyBase64); - const publicKey = await crypto.subtle.importKey( - "spki", - publicKeyBuffer, - { - name: "RSA-OAEP", - hash: "SHA-256", - }, + if (envelope.length < 32) throw new Error("Envelope too short"); + + const enc = envelope.slice(0, 32); + const ct = envelope.slice(32); + + const recipientKey = await suite.kem.importKey( + "raw", + devicePriv.buffer, false, - ["encrypt"], ); - const encrypted = await crypto.subtle.encrypt( - { - name: "RSA-OAEP", - }, - publicKey, - keyBuffer, + const noteKey = await suite.open( + { recipientKey, enc: enc.buffer }, + ct.buffer, ); - return new Uint8Array(encrypted).toBase64(); + return new Uint8Array(noteKey).toBase64(); } -export async function decryptKey( - encryptedKey: string, - privateKey: string, -): Promise { - const encryptedBuffer = Uint8Array.fromBase64(encryptedKey); - const privateKeyBuffer = Uint8Array.fromBase64(privateKey); +// ---------------------------------------------------------------------------- +// Content Encryption (ChaCha20Poly1305) +// ---------------------------------------------------------------------------- - const key = await crypto.subtle.importKey( - "pkcs8", - privateKeyBuffer, - { - name: "RSA-OAEP", - hash: "SHA-256", - }, - false, - ["decrypt"], - ); +export async function encryptData( + data: Uint8Array, + noteKeyBase64: string, +): Promise { + const key = Uint8Array.fromBase64(noteKeyBase64); + const aead = new Chacha20Poly1305(); + const ctx = aead.createEncryptionContext(key.buffer); - const decrypted = await crypto.subtle.decrypt( - { - name: "RSA-OAEP", - }, - key, - encryptedBuffer, + // Per message nonce (12 bytes for standard ChaCha20Poly1305) + const nonce = crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await ctx.seal( + nonce.buffer, + data.buffer.slice( + data.byteOffset, + data.byteOffset + data.byteLength, + ) as ArrayBuffer, + new ArrayBuffer(0), ); - return new Uint8Array(decrypted).toBase64(); + // Prepend nonce + const result = new Uint8Array(12 + ciphertext.byteLength); + result.set(nonce, 0); + result.set(new Uint8Array(ciphertext), 12); + return result; } -export async function encryptData( - data: Uint8Array, - noteKey: string, +export async function decryptData( + encrypted: Uint8Array, + noteKeyBase64: string, ): Promise { - const keyBuffer = Uint8Array.fromBase64(noteKey); - const key = await crypto.subtle.importKey( + const key = Uint8Array.fromBase64(noteKeyBase64); + const aead = new Chacha20Poly1305(); + const ctx = aead.createEncryptionContext(key.buffer); + + // Extract IV from first 12 bytes + const nonce = encrypted.slice(0, 12); + const ciphertext = encrypted.slice(12); + + const decrypted = await ctx.open( + nonce.buffer, + ciphertext.buffer.slice( + ciphertext.byteOffset, + ciphertext.byteOffset + ciphertext.byteLength, + ), + new ArrayBuffer(0), + ); + return new Uint8Array(decrypted); +} + +export const encryptKeyForUser = encryptKeyForDevice; +export const decryptKey = decryptKeyForDevice; +export const generateUserKeys = generateEncryptionKeyPair; + +// ---------------------------------------------------------------------------- +// Password Encryption (PBKDF2 + ChaCha20Poly1305) +// ---------------------------------------------------------------------------- + +export async function encryptWithPassword( + dataBase64: string, + password: string, +): Promise { + const data = Uint8Array.fromBase64(dataBase64); + const salt = globalThis.crypto.getRandomValues(new Uint8Array(16)); + + // Derive key from password + const passwordKey = await crypto.subtle.importKey( "raw", - keyBuffer, - { - name: "AES-GCM", - }, + new TextEncoder().encode(password), + "PBKDF2", false, - ["encrypt"], + ["deriveBits"], ); - const iv = crypto.getRandomValues(new Uint8Array(12)); - const encrypted = await crypto.subtle.encrypt( + const kekBits = await crypto.subtle.deriveBits( { - name: "AES-GCM", - iv, + name: "PBKDF2", + salt, + iterations: 600000, + hash: "SHA-256", }, - key, - data, + passwordKey, + 256, // 32 bytes ); - // Prepend IV to encrypted data - const result = new Uint8Array(iv.length + encrypted.byteLength); - result.set(iv); - result.set(new Uint8Array(encrypted), iv.length); + const aead = new Chacha20Poly1305(); + const ctx = aead.createEncryptionContext(kekBits); - return result; + const nonce = globalThis.crypto.getRandomValues(new Uint8Array(12)); + const ciphertext = await ctx.seal( + nonce.buffer, + data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength), + new ArrayBuffer(0), + ); + + // Pack: salt(16) + nonce(12) + ciphertext + const result = new Uint8Array(16 + 12 + ciphertext.byteLength); + result.set(salt, 0); + result.set(nonce, 16); + result.set(new Uint8Array(ciphertext), 28); + + return result.toBase64(); } -export async function decryptData( - encrypted: Uint8Array, - noteKey: string, -): Promise { - const keyBuffer = Uint8Array.fromBase64(noteKey); - const key = await crypto.subtle.importKey( +export async function decryptWithPassword( + encryptedBase64: string, + password: string, +): Promise { + const encrypted = Uint8Array.fromBase64(encryptedBase64); + + if (encrypted.length < 28) throw new Error("Encrypted data too short"); + + const salt = encrypted.slice(0, 16); + const nonce = encrypted.slice(16, 28); + const ciphertext = encrypted.slice(28); + + const passwordKey = await crypto.subtle.importKey( "raw", - keyBuffer, - { - name: "AES-GCM", - }, + new TextEncoder().encode(password), + "PBKDF2", false, - ["decrypt"], + ["deriveBits"], ); - // Extract IV from first 12 bytes - const iv = encrypted.slice(0, 12); - const data = encrypted.slice(12); - - const decrypted = await crypto.subtle.decrypt( + const kekBits = await crypto.subtle.deriveBits( { - name: "AES-GCM", - iv, + name: "PBKDF2", + salt, + iterations: 600000, + hash: "SHA-256", }, - key, - data, + passwordKey, + 256, ); - return new Uint8Array(decrypted); + const aead = new Chacha20Poly1305(); + const ctx = aead.createEncryptionContext(kekBits); + + try { + const data = await ctx.open( + nonce.buffer, + ciphertext.buffer.slice( + ciphertext.byteOffset, + ciphertext.byteOffset + ciphertext.byteLength, + ), + new ArrayBuffer(0), + ); + return new Uint8Array(data).toBase64(); + } catch { + throw new Error("Incorrect password or corrupted data"); + } } diff --git a/src/lib/loro.svelte.ts b/src/lib/loro.svelte.ts new file mode 100644 index 0000000..7232135 --- /dev/null +++ b/src/lib/loro.svelte.ts @@ -0,0 +1,300 @@ +import { decryptData, encryptData } from "$lib/crypto"; +import { encodeBase64, decodeBase64 } from "@oslojs/encoding"; +import { syncSchemaJson } from "$lib/remote/notes.schemas.ts"; +import { Schema } from "effect"; +import { LoroDoc, LoroText } from "loro-crdt"; +import { unawaited } from "./unawaited.ts"; +import { Temporal } from "temporal-polyfill"; + +export type ConnectionState = + | "connected" + | "connecting" + | "disconnected" + | "reconnecting"; + +export type Doc = LoroDoc<{ + content: LoroText; +}>; + +export class LoroNoteManager { + #noteId: string; + #noteKey: string; + doc: Doc; + #text: LoroText; + #onUpdate: (snapshot: string) => void | Promise; + #eventSource: EventSource | null = null; + #isSyncing = false; + connectionState: ConnectionState = $state("disconnected"); + #retryTimeout: NodeJS.Timeout | null = null; + + static getTextFromDoc(this: void, doc: LoroDoc): LoroText { + return doc.getText("content"); + } + + /** @private */ + private constructor( + noteId: string, + noteKey: string, + onUpdate: (snapshot: string) => Promise, + ) { + console.log("LoroNoteManager created for note:", noteId); + + this.#noteId = noteId; + this.#noteKey = noteKey; + this.doc = new LoroDoc(); + this.doc.setRecordTimestamp(true); + this.#text = LoroNoteManager.getTextFromDoc(this.doc); + this.#onUpdate = onUpdate; + + // Subscribe to changes + this.doc.subscribeLocalUpdates((update) => { + console.log( + "[Loro] Local update detected. Preview:", + this.#text.toString().slice(0, 20), + "Update size:", + update.length, + ); + + // Persist changes + unawaited(this.#persist()); + + // Send local changes immediately + if (this.#isSyncing) { + console.log("[Loro] Sending local update to server"); + unawaited(this.#sendUpdate(update)); + } + }); + } + + /** + * Initialize the manager with an encrypted snapshot + */ + static async create( + this: void, + noteId: string, + noteKey: string, + onUpdate: (snapshot: string) => Promise, + encryptedSnapshot: string | null, + ): Promise { + const manager = new LoroNoteManager(noteId, noteKey, onUpdate); + + if (encryptedSnapshot) { + const encryptedBytes = decodeBase64(encryptedSnapshot); + const decrypted = await decryptData(encryptedBytes, manager.#noteKey); + manager.doc.import(decrypted); + } + return manager; + } + + async #persist() { + const snapshot = await this.getEncryptedSnapshot(); + await this.#onUpdate(snapshot); + } + + /** + * Stop real-time sync + */ + stopSync() { + if (this.#eventSource) { + this.#eventSource.close(); + this.#eventSource = null; + } + if (this.#retryTimeout) { + clearTimeout(this.#retryTimeout); + this.#retryTimeout = null; + } + this.#isSyncing = false; + this.connectionState = "disconnected"; + } + + /** + * Start real-time sync + */ + /** + * Start real-time sync + */ + startSync(): void { + if (this.#isSyncing) return; + this.#isSyncing = true; + this.connectionState = "connecting"; + + // Use SSE endpoint + this.#eventSource = new EventSource(`/client/doc/${this.#noteId}/events`); + + this.#eventSource.onopen = () => { + console.log("[Loro] SSE Connected"); + this.connectionState = "connected"; + // Clear any retry loop if we succeeded + if (this.#retryTimeout) { + clearTimeout(this.#retryTimeout); + this.#retryTimeout = null; + } + }; + + this.#eventSource.onmessage = (event: MessageEvent): void => { + console.debug("[Loro] Received SSE message:", event.data.slice(0, 100)); + try { + const ops = Schema.decodeUnknownSync(syncSchemaJson)(event.data); + if (!Array.isArray(ops)) return; + + for (const op of ops) { + const base64 = op.payload ?? op.encrypted_payload; + if (!base64) { + console.warn("[Loro] Received op without payload:", op); + continue; + } + const updateBytes = decodeBase64(base64); + this.doc.import(updateBytes); + } + // console.debug(`[Loro] Applied ${ops.length} ops`); + } catch (error) { + console.error("Failed to process sync message:", error); + } + }; + + this.#eventSource.onopen = () => { + console.log("[Loro] SSE connected"); + this.connectionState = "connected"; + }; + + this.#eventSource.onerror = (error) => { + console.error("SSE connection error:", error); + // Browser will auto-reconnect usually, but let's be explicit about state + if (this.#eventSource?.readyState === EventSource.CLOSED) { + this.connectionState = "disconnected"; + this.#isSyncing = false; + // Try to reconnect? + this.#scheduleReconnect(); + } else if (this.#eventSource?.readyState === EventSource.CONNECTING) { + this.connectionState = "reconnecting"; + } + }; + } + + #scheduleReconnect() { + if (this.#retryTimeout) return; + this.connectionState = "reconnecting"; + console.log("[Loro] Scheduling reconnect in 3s..."); + this.#retryTimeout = setTimeout(() => { + this.#retryTimeout = null; + this.#isSyncing = false; // reset flag to allow startSync + this.startSync(); + }, 3000); + } + + /** + * Send update to server + */ + async #sendUpdate(update: Uint8Array) { + try { + const payload = encodeBase64(update); + const actorId = this.doc.peerIdStr; + + // Mock Op structure + const op = { + op_id: crypto.randomUUID(), + actor_id: actorId, + lamport_ts: Date.now(), // Approximate ordering + encrypted_payload: payload, + signature: "TODO", // Client signature! + }; + + await fetch(`/client/doc/${this.#noteId}/push`, { + method: "POST", + body: JSON.stringify({ op }), + headers: { "Content-Type": "application/json" }, + }); + } catch (error) { + console.error("Failed to send update:", error); + } + } + + /** + * Get encrypted snapshot for storage + */ + async getEncryptedSnapshot(): Promise { + const snapshot = this.doc.export({ + mode: "snapshot", + }) as Uint8Array; + const encrypted = await encryptData(snapshot, this.#noteKey); + return encodeBase64(encrypted); + } + + /** + * Get the current version/frontiers of the document + */ + getVersion() { + return this.doc.version(); + } + + /** + * Get frontiers (latest version points) of the document + */ + getFrontiers() { + return this.doc.frontiers(); + } + + /** + * Get the text content + */ + getText(): string { + return this.#text.toString(); + } + + /** + * Get version history with user attribution + * Returns an array of version snapshots traversing the oplog + */ + getHistory(): HistoryEntry[] { + const history: HistoryEntry[] = []; + + // Get all changes from the oplog + const changes = this.doc.getAllChanges(); + const currentText = this.#text.toString(); + + // Traverse all changes from all peers + for (const [peerId, peerChanges] of changes) { + for (const change of peerChanges) { + history.push({ + version: change.lamport, + // Loro timestamps are in seconds, convert to milliseconds + timestamp: change.timestamp + ? Temporal.Instant.fromEpochMilliseconds(change.timestamp * 1000) + : Temporal.Instant.fromEpochMilliseconds(0), + preview: currentText.slice(0, 100), + peerId, + }); + } + } + + // Sort by lamport timestamp descending (most recent first) + history.sort((a, b) => b.version - a.version); + + // If no changes found, return current state as fallback + if (history.length === 0) { + const currentVersion = this.doc.version(); + history.push({ + version: currentVersion.get(this.doc.peerId) ?? 0, + timestamp: Temporal.Now.instant(), + preview: currentText.slice(0, 100), + peerId: this.doc.peerId.toString(), + }); + } + + return history; + } + + /** + * Subscribe to history changes (live updates) + */ + subscribeToHistory(callback: () => void): () => void { + return this.doc.subscribe(callback); + } +} + +export interface HistoryEntry { + version: number; + timestamp: Temporal.Instant; + preview: string; + peerId: string; +} diff --git a/src/lib/loro.ts b/src/lib/loro.ts deleted file mode 100644 index 61e3e26..0000000 --- a/src/lib/loro.ts +++ /dev/null @@ -1,276 +0,0 @@ -import { decryptData, encryptData } from "$lib/crypto"; -import { syncSchemaJson } from "$lib/remote/notes.schemas.ts"; -import { sync } from "$lib/remote/sync.remote.ts"; -import { Chunk, Effect, Fiber, Function, PubSub, Schema, Stream } from "effect"; -import diff from "fast-diff"; -import { LoroDoc, LoroText, type Frontiers } from "loro-crdt"; - -export type Doc = LoroDoc<{ - content: LoroText; -}>; - -export class LoroNoteManager { - #noteId: string; - #noteKey: string; - #doc: Doc; - #text: LoroText; - #onUpdate: (snapshot: string) => void | Promise; - #eventSource: EventSource | null = null; - #isSyncing = false; - - #outgoingHub: PubSub.PubSub; - #persistenceHub: PubSub.PubSub; - - #persistenceFiber: Fiber.RuntimeFiber; - #outgoingFiber: Fiber.RuntimeFiber | null = null; - #incomingFiber: Fiber.RuntimeFiber | null = null; - - constructor( - noteId: string, - noteKey: string, - onUpdate?: (snapshot: string) => void | Promise, - ) { - this.#noteId = noteId; - this.#noteKey = noteKey; - this.#doc = new LoroDoc(); - this.#text = this.#doc.getText("content"); - this.#onUpdate = onUpdate ?? Function.constVoid; - - // Initialize frontiers - this.#lastFrontiers = this.#doc.frontiers(); - - // 1. Init Hubs - this.#outgoingHub = Effect.runSync(PubSub.unbounded()); - this.#persistenceHub = Effect.runSync(PubSub.unbounded()); - - // 2. Persistence Loop (Debounced Snapshot) - const persistenceStream = Stream.fromPubSub(this.#persistenceHub).pipe( - Stream.debounce("500 millis"), - Stream.runForEach(() => - Effect.promise(async () => { - const snapshot = await this.getEncryptedSnapshot(); - await this.#onUpdate(snapshot); - }), - ), - ); - this.#persistenceFiber = Effect.runFork(persistenceStream); - - // Subscribe to changes - this.#doc.subscribe((event) => { - // Notify content listeners - const content = this.getContent(); - this.#contentListeners.forEach((listener) => { - listener(content); - }); - - // Publish persistence signal - Effect.runSync(this.#persistenceHub.publish(null)); - - // Publish local ops for sync - if (event.by === "local") { - const frontiers = this.#doc.frontiers(); - try { - const update = this.#doc.export({ - mode: "shallow-snapshot", - frontiers: this.#lastFrontiers, - }); - this.#lastFrontiers = frontiers; - if (update.length > 0) { - Effect.runSync(this.#outgoingHub.publish(update)); - } - } catch (e) { - console.error("Error exporting update", e); - } - } - }); - } - - destroy() { - this.stopSync(); - Effect.runFork(Fiber.interrupt(this.#persistenceFiber)); - } - - #contentListeners: ((content: string) => void)[] = []; - - /** - * Subscribe to content changes - */ - subscribeToContent(listener: (content: string) => void) { - this.#contentListeners.push(listener); - // Return unsubscribe function - return () => { - this.#contentListeners = this.#contentListeners.filter( - (l) => l !== listener, - ); - }; - } - - /** - * Initialize the manager with an encrypted snapshot - */ - async init(encryptedSnapshot?: string) { - if (encryptedSnapshot) { - await this.loadEncryptedSnapshot(encryptedSnapshot); - this.#lastFrontiers = this.#doc.frontiers(); - } - } - - #lastFrontiers: Frontiers; - - /** - * Start real-time sync - */ - startSync(): void { - if (this.#isSyncing) return; - this.#isSyncing = true; - - this.#eventSource = new EventSource(`/api/sync/${this.#noteId}`); - - // 3. Incoming Loop (Remote -> Loro) - const incomingStream = Stream.async((emit) => { - if (this.#eventSource) { - this.#eventSource.onmessage = (event: MessageEvent): void => { - try { - const data = Schema.decodeSync(syncSchemaJson)(event.data); - - for (const update of data.updates) { - const updateBytes = Uint8Array.fromBase64(update); - void emit(Effect.succeed(Chunk.make(updateBytes))); - } - } catch (error) { - console.error("Failed to process sync message:", error); - } - }; - - this.#eventSource.onerror = (error) => { - console.error("SSE connection error:", error); - this.#eventSource?.close(); - this.#isSyncing = false; - }; - } - }).pipe( - Stream.runForEach((update) => - Effect.sync(() => { - this.#doc.import(update); - }), - ), - ); - this.#incomingFiber = Effect.runFork(incomingStream); - - // 4. Outgoing Loop (Local -> Network) - const outgoingStream = Stream.fromPubSub(this.#outgoingHub).pipe( - Stream.groupedWithin(100, "500 millis"), - Stream.runForEach((chunk) => - Effect.promise(async () => { - if (Chunk.isEmpty(chunk)) return; - await this.#sendUpdates(Chunk.toReadonlyArray(chunk)); - }), - ), - ); - this.#outgoingFiber = Effect.runFork(outgoingStream); - } - - /** - * Stop real-time sync - */ - stopSync() { - if (this.#eventSource) { - this.#eventSource.close(); - this.#eventSource = null; - } - if (this.#incomingFiber) { - Effect.runFork(Fiber.interrupt(this.#incomingFiber)); - this.#incomingFiber = null; - } - if (this.#outgoingFiber) { - Effect.runFork(Fiber.interrupt(this.#outgoingFiber)); - this.#outgoingFiber = null; - } - this.#isSyncing = false; - } - - /** - * Send update to server - */ - async #sendUpdates(updates: readonly Uint8Array[]) { - try { - await sync({ - noteId: this.#noteId, - updates: updates.map((u) => u.toBase64()), - }); - } catch (error) { - console.error("Failed to send update:", error); - } - } - - /** - * Get current text content - */ - getContent(): string { - return this.#text.toString(); - } - - /** - * Update text content using diffs - */ - updateContent(newContent: string) { - const currentContent = this.#text.toString(); - if (currentContent === newContent) return; - - console.debug("[Loro] Updating content with diff..."); - - // Calculate diff - const diffs = diff(currentContent, newContent); - - let index = 0; - for (const [type, text] of diffs) { - switch (type) { - // DELETE - case -1: { - this.#text.delete(index, text.length); - break; - } - - // EQUAL - case 0: { - index += text.length; - break; - } - - // INSERT - case 1: { - this.#text.insert(index, text); - index += text.length; - break; - } - } - } - - this.#doc.commit(); - } - - /** - * Get encrypted snapshot for storage - */ - async getEncryptedSnapshot(): Promise { - const snapshot = this.#doc.export({ - mode: "snapshot", - }) as Uint8Array; - const encrypted = await encryptData(snapshot, this.#noteKey); - return encrypted.toBase64(); - } - - /** - * Load from encrypted snapshot - */ - async loadEncryptedSnapshot(encryptedSnapshot: string) { - try { - const encryptedBytes = Uint8Array.fromBase64(encryptedSnapshot); - const decrypted = await decryptData(encryptedBytes, this.#noteKey); - this.#doc.import(decrypted); - } catch (error) { - console.error("Failed to load encrypted snapshot:", error); - throw error; - } - } -} diff --git a/src/lib/noteId.ts b/src/lib/noteId.ts new file mode 100644 index 0000000..fe30255 --- /dev/null +++ b/src/lib/noteId.ts @@ -0,0 +1,53 @@ +/** + * Utilities for creating and parsing domain-prefixed note IDs + * Format: {base64url(origin)}~{uuid} + */ + +/** + * Create a new note ID with embedded origin domain + */ +export function createNoteId(serverDomain: string): string { + const uuid = crypto.randomUUID(); + + const domainB64 = Uint8Array.from(serverDomain, (c) => + c.charCodeAt(0), + ).toBase64({ alphabet: "base64url", omitPadding: true }); + + return `${domainB64}~${uuid}`; +} + +/** + * Parse a note ID to extract origin domain and UUID + */ +export function parseNoteId(id: string): { + origin: string; + uuid: string; + fullId: string; +} { + const sepIndex = id.indexOf("~"); + if (sepIndex === -1) throw new Error(`Invalid portable ID format: ${id}`); + + const domainB64 = id.slice(0, sepIndex); + const uuid = id.slice(sepIndex + 1); + + if (!domainB64 || !uuid) throw new Error(`Malformed portable ID: ${id}`); + + // Decode base64url directly to string without intermediate array + const origin = new TextDecoder().decode( + Uint8Array.fromBase64(domainB64, { alphabet: "base64url" }), + ); + + return { origin, uuid, fullId: id }; +} + +/** + * Check if a note ID is from the local server + */ +export function isLocalNote(noteId: string, currentDomain: string): boolean { + try { + const { origin } = parseNoteId(noteId); + return !origin || origin === currentDomain; + } catch { + return false; + } +} diff --git a/src/lib/remote/accounts.remote.ts b/src/lib/remote/accounts.remote.ts index 11ef790..f8245c1 100644 --- a/src/lib/remote/accounts.remote.ts +++ b/src/lib/remote/accounts.remote.ts @@ -1,17 +1,59 @@ -import { form, getRequestEvent } from "$app/server"; +import { command, form, getRequestEvent } from "$app/server"; import * as auth from "$lib/server/auth.ts"; import { db } from "$lib/server/db/index.ts"; import * as table from "$lib/server/db/schema.ts"; import { hash, verify } from "@node-rs/argon2"; -import { fail, invalid, redirect } from "@sveltejs/kit"; +import { error, redirect } from "@sveltejs/kit"; import { eq } from "drizzle-orm"; -import { Redacted, Schema } from "effect"; -import { loginSchema, signupSchema } from "./accounts.schema.ts"; +import { Redacted } from "effect"; +import { + changePasswordSchema, + loginSchema, + setupEncryptionSchema, + signupSchema, +} from "./accounts.schema.ts"; + +export const setupEncryption = command( + setupEncryptionSchema, + async ({ _password: password, publicKey, privateKeyEncrypted }) => { + const authData = auth.guardLogin(); + const user = authData.user; + + // Verify password one last time + const userData = await db + .select() + .from(table.users) + .where(eq(table.users.id, user.id)); + + const existingUser = userData[0]; + + if (!existingUser) { + error(400, "User not found"); + } + + const validPassword = await verify( + existingUser.passwordHash, + password.pipe(Redacted.value), + ); + + if (!validPassword) { + error(400, "Incorrect password"); + } + + await db + .update(table.users) + .set({ + publicKey, + privateKeyEncrypted, + }) + .where(eq(table.users.id, user.id)); + }, +); export const login = form( loginSchema, async ({ username, _password: password }) => { - const { cookies } = getRequestEvent(); + const { cookies, url } = getRequestEvent(); const results = await db .select() @@ -20,7 +62,7 @@ export const login = form( const existingUser = results.at(0); if (!existingUser) { - invalid("Incorrect username or password"); + error(400, "Incorrect username or password"); } const validPassword = await verify( @@ -34,14 +76,18 @@ export const login = form( }, ); if (!validPassword) { - invalid("Incorrect username or password"); + error(400, "Incorrect username or password"); } const sessionToken = auth.generateSessionToken(); const session = await auth.createSession(sessionToken, existingUser.id); auth.setSessionTokenCookie(cookies, sessionToken, session.expiresAt); - return redirect(302, "/"); + // Redirect to the original destination if provided, otherwise go home + const redirectTo = url.searchParams.get("redirectTo") ?? "/"; + // Validate redirectTo to prevent open redirect attacks + const safeRedirect = redirectTo.startsWith("/") ? redirectTo : "/"; + redirect(302, safeRedirect); }, ); @@ -73,20 +119,30 @@ export const signup = form( const session = await auth.createSession(sessionToken, id); auth.setSessionTokenCookie(cookies, sessionToken, session.expiresAt); } catch { - return fail(500, { message: "An error has occurred" }); + error(500, "An error has occurred"); } redirect(302, "/"); }, ); -export const logout = form( - Schema.Struct({}).pipe(Schema.standardSchemaV1), - async () => { - const { cookies } = getRequestEvent(); +export const changePassword = form( + changePasswordSchema, + async ({ _password: password, privateKeyEncrypted }) => { const authData = auth.guardLogin(); - await auth.invalidateSession(authData.session.userId); - auth.deleteSessionTokenCookie(cookies); - redirect(302, "/login"); + const passwordHash = await hash(password.pipe(Redacted.value), { + memoryCost: 19456, + timeCost: 2, + outputLen: 32, + parallelism: 1, + }); + + await db + .update(table.users) + .set({ + passwordHash, + privateKeyEncrypted, + }) + .where(eq(table.users.id, authData.session.userId)); }, ); diff --git a/src/lib/remote/accounts.schema.ts b/src/lib/remote/accounts.schema.ts index 3ab15e1..6f91bea 100644 --- a/src/lib/remote/accounts.schema.ts +++ b/src/lib/remote/accounts.schema.ts @@ -56,3 +56,26 @@ export const SignupSchema = Schema.Struct({ }); export const signupSchema = SignupSchema.pipe(Schema.standardSchemaV1); + +export const ChangePasswordSchema = Schema.Struct({ + _password: PasswordSchema.annotations({ + title: "New Password", + }), + privateKeyEncrypted: Schema.String, +}); + +export const changePasswordSchema = ChangePasswordSchema.pipe( + Schema.standardSchemaV1, +); + +export const SetupEncryptionSchema = Schema.Struct({ + _password: PasswordSchema.annotations({ + title: "Current Password", + }), + publicKey: Schema.String, + privateKeyEncrypted: Schema.String, +}); + +export const setupEncryptionSchema = SetupEncryptionSchema.pipe( + Schema.standardSchemaV1, +); diff --git a/src/lib/remote/federation.remote.ts b/src/lib/remote/federation.remote.ts new file mode 100644 index 0000000..2cd83ce --- /dev/null +++ b/src/lib/remote/federation.remote.ts @@ -0,0 +1,219 @@ +import { command, getRequestEvent } from "$app/server"; +import { db } from "$lib/server/db/index.ts"; +import { documents, members } from "$lib/server/db/schema.ts"; +import { error } from "@sveltejs/kit"; +import { env } from "$env/dynamic/private"; +import { parseNoteId } from "$lib/noteId.ts"; +import { getServerIdentity } from "$lib/server/identity.ts"; +import { encryptKeyForUser, sign } from "$lib/crypto.ts"; +import { eq } from "drizzle-orm"; +import { Schema } from "effect"; + +// Schema for joinFederatedNote command +const joinFederatedNoteSchema = Schema.Struct({ + noteId: Schema.String, + originServer: Schema.String, + preComputedKey: Schema.optional(Schema.String), +}).pipe(Schema.standardSchemaV1); + +export const joinFederatedNote = command( + joinFederatedNoteSchema, + async ({ noteId, originServer, preComputedKey }) => { + console.log("=== FEDERATION JOIN START ==="); + console.log(" noteId:", noteId); + console.log(" originServer:", originServer); + + const event = getRequestEvent(); + const user = event.locals.user; + + try { + const currentDomain = env["SERVER_DOMAIN"] ?? "localhost:5173"; + const { uuid } = parseNoteId(noteId); + + // Skip DB check if no user + if (user) { + try { + // Check if already joined (check both full ID and uuid) + let existingDoc = await db.query.documents.findFirst({ + where: eq(documents.id, noteId), + with: { + members: { + where: (members) => eq(members.userId, user.id), + }, + }, + }); + + existingDoc ??= await db.query.documents.findFirst({ + where: eq(documents.id, uuid), + with: { members: { where: (m) => eq(m.userId, user.id) } }, + }); + + const memberEntry = existingDoc?.members[0]; + const hasKey = !!memberEntry?.encryptedKeyEnvelope; + + if (existingDoc && hasKey) { + console.log(`Already joined note ${noteId} (Key found)`); + return { success: true, alreadyJoined: true }; + } + } catch (e) { + console.error("DB check failed", e); + } + } + + // Call origin server's join endpoint + const joinUrl = `http://${originServer}/federation/doc/${encodeURIComponent(noteId)}/join`; + + // Get server identity for signing + const serverIdentity = await getServerIdentity(); + const timestamp = Date.now().toString(); + const userHandle = user + ? `@${user.username}:${currentDomain}` + : `@anonymous:${currentDomain}`; + + const requestBody = { + requesting_server: currentDomain, + users: user ? [userHandle] : [], // Send empty user list if anonymous + }; + + const message = `${currentDomain}:${timestamp}:${JSON.stringify(requestBody)}`; + const signature = await sign( + new TextEncoder().encode(message), + serverIdentity.privateKey, + ); + + const response = await fetch(joinUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-notes-signature": signature, + "x-notes-timestamp": timestamp, + "x-notes-domain": currentDomain, + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorText = await response.text(); + error(response.status, `Failed to join note: ${errorText}`); + } + + const joinData = await response.json(); + + // Handle Password Protection + // If server returned a passwordEncryptedKey AND we don't have a preComputedKey provided yet: + if (joinData.passwordEncryptedKey && !preComputedKey) { + console.log( + " [Federation] Note is password protected. Client must unlock.", + ); + return { + success: false, + status: "needs_password", + passwordEncryptedKey: joinData.passwordEncryptedKey, + }; + } + + let encryptedKeyEnvelope: string | undefined; + + if (preComputedKey) { + console.log( + " [Federation] Using pre-computed key envelope provided by client.", + ); + encryptedKeyEnvelope = preComputedKey; + } else { + // Robust envelope finding (User Envelope or Public Raw Key) + // Check for RAW KEY first (Open Public) + if (joinData.rawKey) { + console.log(" [Federation] Note is Open Public. Using Raw Key."); + + if (user?.publicKey) { + encryptedKeyEnvelope = await encryptKeyForUser( + joinData.rawKey, + user.publicKey, + ); + } else { + // Anonymous: We don't need an envelope because we don't save to DB. + // But we might want to return the rawKey directly in the response (handled by ...joinData) + console.log( + " [Federation] Anonymous user: Skipping envelope creation.", + ); + } + } else { + // Normal E2EE Envelope Logic + let myEnvelope = joinData.envelopes?.find( + (e: any) => + (user && e.user_id === userHandle) ?? + (user && e.user_id === user.id) ?? + (user && e.user_id === `@${user.username}`) ?? + (user && e.user_id === user.username), + ); + + if ( + !myEnvelope && + joinData.envelopes?.length === 1 && + joinData.envelopes[0].user_id + ) { + myEnvelope = joinData.envelopes[0]; + } + + encryptedKeyEnvelope = myEnvelope?.encrypted_key; + } + } + + // If logged in, we expect an envelope/key to save + if (user) { + if (!encryptedKeyEnvelope) { + console.error(" [Federation] No encrypted key envelope found!"); + } + + // Store document metadata locally + await db + .insert(documents) + .values({ + id: noteId, + hostServer: originServer, + ownerId: joinData.ownerId ?? user.id, + title: joinData.title ?? "Federated Note", + accessLevel: joinData.accessLevel ?? "private", + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoUpdate({ + target: documents.id, + set: { + title: joinData.title ?? "Federated Note", + accessLevel: joinData.accessLevel ?? "private", + updatedAt: new Date(), + }, + }); + + // Store member relationship + await db + .insert(members) + .values({ + docId: noteId, + userId: user.id, + deviceId: "default", + role: joinData.role ?? "writer", + encryptedKeyEnvelope: encryptedKeyEnvelope, + createdAt: new Date(), + }) + .onConflictDoUpdate({ + target: [members.docId, members.userId, members.deviceId], + set: { + encryptedKeyEnvelope: encryptedKeyEnvelope, + role: joinData.role ?? "writer", + }, + }); + } + + console.log(`Successfully joined note ${uuid} from ${originServer}`); + return { success: true, alreadyJoined: false, ...joinData }; + } catch (err) { + console.error("Federation join error:", err); + if (err instanceof Error) { + console.error("Stack:", err.stack); + } + error(500, `Failed to join federated note: ${err}`); + } + }, +); diff --git a/src/lib/remote/notes.remote.ts b/src/lib/remote/notes.remote.ts index cfdc0d7..959aa61 100644 --- a/src/lib/remote/notes.remote.ts +++ b/src/lib/remote/notes.remote.ts @@ -2,9 +2,16 @@ import { command, query } from "$app/server"; import type { NoteOrFolder } from "$lib/schema.ts"; import { requireLogin } from "$lib/server/auth.ts"; import { db } from "$lib/server/db/index.ts"; -import { notes } from "$lib/server/db/schema.ts"; +import { + notes, + documents, + members, + noteShares, +} from "$lib/server/db/schema.ts"; import { error } from "@sveltejs/kit"; -import { and, eq } from "drizzle-orm"; +import { and, eq, ne } from "drizzle-orm"; +import { env } from "$env/dynamic/private"; +import { createNoteId } from "$lib/noteId.ts"; import { createNoteSchema, deleteNoteSchema, @@ -15,20 +22,92 @@ import { export const getNotes = query(async (): Promise => { const { user } = requireLogin(); + // 1. Get owned notes from 'notes' table const userNotes = await db.query.notes.findMany({ where: (notes) => eq(notes.ownerId, user.id), + with: { + document: true, + }, }); - return userNotes.map( + const notesList: NoteOrFolder[] = userNotes.map( (n) => ({ ...n, - content: "", // Will be decrypted when selected + content: "", order: n.order, createdAt: new Date(n.createdAt), updatedAt: new Date(n.updatedAt), + serverEncryptedKey: n.document.serverEncryptedKey ?? null, }) satisfies NoteOrFolder, ); + + // 2. Get shared/federated notes where user is a member + // These are stored in 'documents' table and linked via 'members' + // We need to join them. + // Query members where userId = user.id + const memberships = await db.query.members.findMany({ + where: (members) => eq(members.userId, user.id), + with: { + document: true, + }, + }); + + // Filter out any that might overlap with owned notes (though normally shouldn't) + // And map to NoteOrFolder + for (const m of memberships) { + // Check if already in list (owned notes might be in members too?) + if (notesList.some((n) => n.id === m.document.id)) continue; + + // Map to NoteOrFolder structure + // We treat them as root-level notes for now (parentId: null) + // We get encryptedKey from the member envelope + if (m.encryptedKeyEnvelope) { + const documentKey = m.encryptedKeyEnvelope; + + notesList.push({ + id: m.document.id, + title: m.document.title ?? "Shared Note", + ownerId: m.document.ownerId, + encryptedKey: documentKey, + isFolder: false, // Default for shared docs + order: 0, + parentId: null, + createdAt: m.document.createdAt, + updatedAt: m.document.updatedAt, + content: "", + accessLevel: m.document.accessLevel, + loroSnapshot: null, // Snapshots for federated notes handled separately? + }); + } + } + + return notesList; +}); + +export interface SharedNote { + id: string; + title: string; + hostServer: string; + ownerId: string; + accessLevel: string; +} + +export const getSharedNotes = query(async (): Promise => { + requireLogin(); + + // Get documents from remote servers (not local) + const sharedDocs = await db.query.documents.findMany({ + where: (documents) => ne(documents.hostServer, "local"), + }); + + return sharedDocs.map((doc) => ({ + id: doc.id, + title: doc.title ?? "Untitled", + hostServer: doc.hostServer, + ownerId: doc.ownerId, + accessLevel: doc.accessLevel, + })); }); /** @todo Switch to form? */ @@ -37,6 +116,7 @@ export const createNote = command( async ({ title, encryptedKey, + serverEncryptedKey, parentId, isFolder, }): Promise> => { @@ -47,7 +127,22 @@ export const createNote = command( error(400, "Missing required fields"); } - const id = crypto.randomUUID(); + const serverDomain = env["SERVER_DOMAIN"] ?? "localhost:5173"; + const id = createNoteId(serverDomain); + + // Dual-write to documents table to support federatedOps + await db.insert(documents).values({ + id, + hostServer: "local", + ownerId: user.id, + title: title, + accessLevel: "private", // Default + documentKeyEncrypted: null, // Local notes use encryptedKey in notes table for now? Or should we populate this? + serverEncryptedKey, // Key Broker Escrow + // Ideally we migrate to using documents entirely, but for now dual-write. + createdAt: new Date(), + updatedAt: new Date(), + }); await db.insert(notes).values({ id, @@ -89,9 +184,22 @@ export const deleteNote = command( if (!note || note.ownerId !== user.id) error(404, "Not found"); + // Cascading delete + // 1. Delete shares (depend on notes) + await db.delete(noteShares).where(eq(noteShares.noteId, noteId)); + + // 2. Delete members (depend on documents) + await db.delete(members).where(eq(members.docId, noteId)); + + // 3. Delete note (the main target) await db.delete(notes).where(eq(notes.id, noteId)); + + // 4. Delete document entry (if exists, depends on nothing else usually, but members depend on it) + // We do this last or after members + await db.delete(documents).where(eq(documents.id, noteId)); } catch (err) { console.error("Delete note error:", err); + // Don't expose internal DB errors, but useful to know if FK failed error(500, "Failed to delete note"); } }, @@ -104,6 +212,7 @@ export const updateNote = command( title, loroSnapshot, parentId, + serverEncryptedKey, }): Promise> => { const { user } = requireLogin(); @@ -111,6 +220,7 @@ export const updateNote = command( // Verify ownership const existingNote = await db.query.notes.findFirst({ where: eq(notes.id, noteId), + with: { document: true }, }); if (!existingNote || existingNote.ownerId !== user.id) { @@ -128,6 +238,14 @@ export const updateNote = command( }) .where(eq(notes.id, noteId)); + // Update serverEncryptedKey in documents table if provided + if (serverEncryptedKey) { + await db + .update(documents) + .set({ serverEncryptedKey, updatedAt: new Date() }) + .where(eq(documents.id, noteId)); + } + const updated = await db.query.notes.findFirst({ where: eq(notes.id, noteId), }); diff --git a/src/lib/remote/notes.schemas.ts b/src/lib/remote/notes.schemas.ts index 696c247..edffd55 100644 --- a/src/lib/remote/notes.schemas.ts +++ b/src/lib/remote/notes.schemas.ts @@ -5,6 +5,7 @@ export const CreateNoteSchema = Schema.Struct({ parentId: Schema.String.pipe(Schema.NullOr), isFolder: Schema.Boolean, encryptedKey: Schema.String, + serverEncryptedKey: Schema.String, }); export const createNoteSchema = CreateNoteSchema.pipe(Schema.standardSchemaV1); @@ -16,6 +17,7 @@ export const UpdateNoteSchema = Schema.Struct({ title: Schema.optional(Schema.String), loroSnapshot: Schema.optional(Schema.String), parentId: Schema.optional(Schema.String.pipe(Schema.NullOr)), + serverEncryptedKey: Schema.optional(Schema.String), }); export const updateNoteSchema = UpdateNoteSchema.pipe(Schema.standardSchemaV1); @@ -30,7 +32,7 @@ export const reorderNotesSchema = ReorderNotesSchema.pipe( export const SyncSchema = Schema.Struct({ noteId: Schema.String, - updates: Schema.Array(Schema.String), + update: Schema.String, }); export const syncSchemaJson = Schema.parseJson(SyncSchema); export const syncSchema = SyncSchema.pipe(Schema.standardSchemaV1); diff --git a/src/lib/remote/sync.remote.ts b/src/lib/remote/sync.remote.ts index 77d0a78..a3d1d26 100644 --- a/src/lib/remote/sync.remote.ts +++ b/src/lib/remote/sync.remote.ts @@ -7,7 +7,7 @@ import { eq } from "drizzle-orm"; import { Schema } from "effect"; import { syncSchema, syncSchemaJson } from "./notes.schemas.ts"; -export const sync = command(syncSchema, async ({ noteId, updates }) => { +export const sync = command(syncSchema, async ({ noteId, update }) => { const { locals } = getRequestEvent(); const user = locals.user; @@ -21,11 +21,11 @@ export const sync = command(syncSchema, async ({ noteId, updates }) => { if (!note || note.ownerId !== user.id) error(404, "Not found"); - console.debug("Syncing", noteId); + console.log("Syncing", noteId); // Broadcast update to all connected clients // The update is expected to be a base64 string of the binary update - broadcast(noteId, Schema.encodeSync(syncSchemaJson)({ noteId, updates })); + broadcast(noteId, Schema.encodeSync(syncSchemaJson)({ noteId, update })); } catch (err) { console.error("Sync update error:", err); error(500, "Failed to process update"); diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 9606570..ea11064 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -11,6 +11,8 @@ const NoteBaseSchema = Schema.Struct({ order: Schema.Number, createdAt: Schema.Date, updatedAt: Schema.Date, + accessLevel: Schema.optional(Schema.String), + serverEncryptedKey: Schema.optional(Schema.NullOr(Schema.String)), }); export const NoteSchema = Schema.extend( @@ -37,6 +39,6 @@ export type NoteOrFolder = Note | Folder; export interface User { id: string; username: string; - publicKey: string; + publicKey: string | null; privateKeyEncrypted: string; } diff --git a/src/lib/server/auth.ts b/src/lib/server/auth.ts index fbbafbb..87964cc 100644 --- a/src/lib/server/auth.ts +++ b/src/lib/server/auth.ts @@ -51,20 +51,32 @@ export type AuthData = NoAuthData | SomeAuthData; export async function validateSessionToken(token: string): Promise { const sessionId = encodeHexLowerCase(sha256(new TextEncoder().encode(token))); - const [result] = await db - .select({ - // Adjust user table here to tweak returned data - user: { - id: table.users.id, - username: table.users.username, - publicKey: table.users.publicKey, - privateKeyEncrypted: table.users.privateKeyEncrypted, - }, - session: table.sessions, - }) - .from(table.sessions) - .innerJoin(table.users, eq(table.sessions.userId, table.users.id)) - .where(eq(table.sessions.token, sessionId)); + + let result; + try { + const rows = await db + .select({ + // Adjust user table here to tweak returned data + user: { + id: table.users.id, + username: table.users.username, + publicKey: table.users.publicKey, + privateKeyEncrypted: table.users.privateKeyEncrypted, + }, + session: table.sessions, + }) + .from(table.sessions) + .innerJoin(table.users, eq(table.sessions.userId, table.users.id)) + .where(eq(table.sessions.token, sessionId)); + result = rows[0]; + } catch (e) { + console.error( + "Session validation DB error (DB might be missing/cleared):", + e, + ); + // Treat DB errors as no session -> force logout/login + return { session: undefined, user: undefined }; + } if (result === undefined) { return { session: undefined, user: undefined }; @@ -97,6 +109,10 @@ export async function invalidateSession(sessionId: string) { await db.delete(table.sessions).where(eq(table.sessions.token, sessionId)); } +export async function invalidateUserSessions(userId: string) { + await db.delete(table.sessions).where(eq(table.sessions.userId, userId)); +} + export function setSessionTokenCookie( cookies: Cookies, token: string, diff --git a/src/lib/server/db/schema.ts b/src/lib/server/db/schema.ts index e7f8a69..70764d3 100644 --- a/src/lib/server/db/schema.ts +++ b/src/lib/server/db/schema.ts @@ -1,11 +1,29 @@ -import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"; +import { + sqliteTable, + text, + integer, + primaryKey, +} from "drizzle-orm/sqlite-core"; export const users = sqliteTable("users", { id: text("id").primaryKey(), username: text("username").notNull().unique(), + handle: text("handle").unique(), // Federated handle, e.g. "alice" passwordHash: text("password_hash").notNull(), - publicKey: text("public_key").notNull(), - privateKeyEncrypted: text("private_key_encrypted").notNull(), + publicKey: text("public_key"), // Ed25519 public key + privateKeyEncrypted: text("private_key_encrypted").notNull(), // Existing field, encrypted with password + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), +}); + +export const devices = sqliteTable("devices", { + id: text("id").primaryKey(), + userId: text("user_id") + .notNull() + .references(() => users.id), + deviceId: text("device_id").notNull(), + publicKey: text("public_key").notNull(), // Device specific public key (X25519/Ed25519) createdAt: integer("created_at", { mode: "timestamp" }) .notNull() .$defaultFn(() => new Date()), @@ -19,6 +37,58 @@ export const sessions = sqliteTable("sessions", { expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(), }); +// The core document table for federated notes +export const documents = sqliteTable("documents", { + id: text("id").primaryKey(), // UUID + hostServer: text("host_server").notNull(), // e.g. "home.example.com" or "local" + ownerId: text("owner_id").notNull(), // Federated ID or local user ID + title: text("title"), + accessLevel: text("access_level").notNull().default("private"), + documentKeyEncrypted: text("document_key_encrypted"), + serverEncryptedKey: text("server_encrypted_key"), // Encrypted for the Home Server (Key Broker) + passwordEncryptedKey: text("password_encrypted_key"), // Encrypted with Note Password (for password-protected access) + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + updatedAt: integer("updated_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), +}); + +export const members = sqliteTable( + "members", + { + docId: text("doc_id") + .notNull() + .references(() => documents.id), + userId: text("user_id").notNull(), // Federated ID + role: text("role").notNull().default("writer"), // reader, writer, owner + encryptedKeyEnvelope: text("encrypted_key_envelope"), // Encrypted doc key for this user/device + deviceId: text("device_id").notNull(), // If key is per-device, make it not null as per PK + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), + }, + (t) => [primaryKey({ columns: [t.docId, t.userId, t.deviceId] })], +); + +export const federatedOps = sqliteTable("federated_ops", { + id: text("id").primaryKey(), // composite or specific ID? + docId: text("doc_id") + .notNull() + .references(() => documents.id), + opId: text("op_id").notNull(), // actor+counter + actorId: text("actor_id").notNull(), + lamportTs: integer("lamport_ts").notNull(), + payload: text("payload").notNull(), // Encrypted blob (base64) + signature: text("signature").notNull(), + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), +}); + +// Keeping existing tables for now to avoid immediate breakage, +// but we might migrate `notes` -> `documents` logic. export const notes = sqliteTable("notes", { id: text("id").primaryKey(), title: text("title").notNull(), @@ -27,6 +97,8 @@ export const notes = sqliteTable("notes", { .references(() => users.id), encryptedKey: text("encrypted_key").notNull(), loroSnapshot: text("loro_snapshot"), + accessLevel: text("access_level").notNull().default("private"), // private, authenticated, open, invite_only + documentKeyEncrypted: text("document_key_encrypted"), // Document key encrypted for owner parentId: text("parent_id"), isFolder: integer("is_folder", { mode: "boolean" }).notNull().default(false), order: integer("order").notNull().default(0), @@ -51,7 +123,77 @@ export const noteShares = sqliteTable("note_shares", { .$defaultFn(() => new Date()), }); +import { relations } from "drizzle-orm"; + +export const usersRelations = relations(users, ({ many }) => ({ + notes: many(notes), + memberships: many(members), +})); + +export const documentsRelations = relations(documents, ({ many }) => ({ + members: many(members), +})); + +export const membersRelations = relations(members, ({ one }) => ({ + document: one(documents, { + fields: [members.docId], + references: [documents.id], + }), + user: one(users, { + fields: [members.userId], + references: [users.id], + }), +})); + +export const notesRelations = relations(notes, ({ one, many }) => ({ + owner: one(users, { + fields: [notes.ownerId], + references: [users.id], + }), + shares: many(noteShares), + document: one(documents, { + fields: [notes.id], + references: [documents.id], + }), +})); + +export const noteSharesRelations = relations(noteShares, ({ one }) => ({ + note: one(notes, { + fields: [noteShares.id], // typo in schema? id is PK. noteId is FK. + references: [notes.id], + }), +})); + +export const joinRequests = sqliteTable("join_requests", { + id: text("id").primaryKey(), // UUID + docId: text("doc_id") + .notNull() + .references(() => documents.id), + userId: text("user_id").notNull(), // The user requesting access + status: text("status").notNull().default("pending"), // pending, approved, rejected + publicKey: text("public_key").notNull(), // The requester's public key (to lock the note key against) + createdAt: integer("created_at", { mode: "timestamp" }) + .notNull() + .$defaultFn(() => new Date()), +}); + +export const joinRequestsRelations = relations(joinRequests, ({ one }) => ({ + document: one(documents, { + fields: [joinRequests.docId], + references: [documents.id], + }), + user: one(users, { + fields: [joinRequests.userId], + references: [users.id], + }), +})); + export type User = typeof users.$inferSelect; +export type Device = typeof devices.$inferSelect; export type Session = typeof sessions.$inferSelect; +export type Document = typeof documents.$inferSelect; +export type Member = typeof members.$inferSelect; +export type FederatedOp = typeof federatedOps.$inferSelect; export type Note = typeof notes.$inferSelect; export type NoteShare = typeof noteShares.$inferSelect; +export type JoinRequest = typeof joinRequests.$inferSelect; diff --git a/src/lib/server/federation.ts b/src/lib/server/federation.ts new file mode 100644 index 0000000..191310a --- /dev/null +++ b/src/lib/server/federation.ts @@ -0,0 +1,156 @@ +/** + * Federation utilities for cross-server communication + */ + +import { encryptKeyForDevice } from "$lib/crypto"; + +export interface RemoteUserIdentity { + id: string; + handle: string; + publicKey: string | null; + devices: { + device_id: string; + public_key: string; + }[]; +} + +/** + * Fetch a user's identity from their home server + * @param handle - Federated handle like @alice:server.com or @bob + * @param requestingDomain - Domain making the request (for relative handles) + */ +export async function fetchUserIdentity( + handle: string, + requestingDomain: string, +): Promise { + // Parse handle to extract user and domain + const cleanHandle = handle.startsWith("@") ? handle.slice(1) : handle; + + let username: string; + let domain: string; + + if (cleanHandle.includes(":")) { + // Federated handle: user:domain.com + const parts = cleanHandle.split(":"); + username = parts[0] ?? ""; + domain = parts.slice(1).join(":"); // Handle domain:port + } else { + // Local handle or just username + username = cleanHandle; + domain = requestingDomain; + } + + if (!username || !domain) { + console.error("Invalid handle format:", handle); + return null; + } + + const protocol = domain.includes("localhost") ? "http" : "https"; + const identityUrl = `${protocol}://${domain}/.well-known/notes-identity/@${username}`; + + try { + const res = await fetch(identityUrl, { + headers: { Accept: "application/json" }, + }); + + if (!res.ok) { + console.error( + `Failed to fetch identity for ${handle}: ${res.status.toFixed()}`, + ); + return null; + } + + const data = (await res.json()) as unknown as RemoteUserIdentity; + return data; + } catch (err) { + console.error(`Error fetching identity for ${handle}:`, err); + return null; + } +} + +/** + * Encrypt a document key for a remote user + * Uses the user's primary public key (or first device key if no user key) + */ +export async function encryptDocumentKeyForUser( + documentKey: string, + identity: RemoteUserIdentity, +): Promise { + // Prefer user's main public key, fallback to first device + const publicKey = identity.publicKey ?? identity.devices[0]?.public_key; + + if (!publicKey) { + console.error(`No public key found for user ${identity.handle}`); + return null; + } + + try { + return await encryptKeyForDevice(documentKey, publicKey); + } catch (err) { + console.error(`Failed to encrypt key for ${identity.handle}:`, err); + return null; + } +} + +/** + * Generate encrypted key envelopes for multiple users + */ +export async function generateKeyEnvelopesForUsers( + documentKey: string, + userHandles: string[], + requestingDomain: string, +): Promise< + { + user_id: string; + encrypted_key: string; + device_id: string; + }[] +> { + const envelopes: { + user_id: string; + encrypted_key: string; + device_id: string; + }[] = []; + + for (const handle of userHandles) { + const identity = await fetchUserIdentity(handle, requestingDomain); + if (!identity) { + console.warn(`Skipping ${handle} - could not fetch identity`); + continue; + } + + // Generate envelope for user's main key + if (identity.publicKey) { + const encryptedKey = await encryptDocumentKeyForUser( + documentKey, + identity, + ); + if (encryptedKey) { + envelopes.push({ + user_id: identity.handle, + encrypted_key: encryptedKey, + device_id: "primary", // Main user key, not device-specific + }); + } + } + + // Optionally generate envelopes for each device + for (const device of identity.devices) { + try { + const encryptedKey = await encryptKeyForDevice( + documentKey, + device.public_key, + ); + envelopes.push({ + user_id: identity.handle, + encrypted_key: encryptedKey, + device_id: device.device_id, + }); + } catch (err) { + console.error(`Failed to encrypt for device ${device.device_id}:`, err); + } + } + } + + return envelopes; +} diff --git a/src/lib/server/identity.ts b/src/lib/server/identity.ts new file mode 100644 index 0000000..deba7fd --- /dev/null +++ b/src/lib/server/identity.ts @@ -0,0 +1,86 @@ +import { env } from "$env/dynamic/private"; +import { + generateSigningKeyPair, + generateEncryptionKeyPair, + sign, +} from "$lib/crypto"; +import { Schema } from "effect"; +import fs from "node:fs/promises"; + +const IDENTITY_FILE = env["SERVER_IDENTITY_FILE"] ?? "server-identity.json"; + +const ServerIdentitySchema = Schema.Struct({ + publicKey: Schema.String, // Ed25519 (Signing) + privateKey: Schema.String, + encryptionPublicKey: Schema.String, // X25519 (Broker Encryption) + encryptionPrivateKey: Schema.String, + domain: Schema.String, +}).pipe(Schema.mutable); + +type ServerIdentity = typeof ServerIdentitySchema.Type; + +const serverIdentityJson = Schema.parseJson(ServerIdentitySchema); + +// Singleton identity +// Gosh plz no... +// TODO: remove this abomination. +let identity: ServerIdentity | null = null; + +export async function getServerIdentity(): Promise { + if (identity) return identity; + + const domain = env["SERVER_DOMAIN"] ?? "localhost:5173"; + + try { + const data = await fs.readFile(IDENTITY_FILE, "utf-8"); + const loaded = Schema.decodeUnknownSync(serverIdentityJson)(data); + + // Backwards compatibility: Generate encryption keys if missing + if (loaded.publicKey && !loaded.encryptionPublicKey) { + console.log("Upgrading server identity with encryption keys..."); + const encParams = await generateEncryptionKeyPair(); + loaded.encryptionPublicKey = encParams.publicKey; + loaded.encryptionPrivateKey = encParams.privateKey; + await fs.writeFile(IDENTITY_FILE, JSON.stringify(loaded, null, 2)); + } + + identity = loaded; + identity.domain = domain; + + return identity; + } catch { + console.warn("No existing server identity found, generating new one..."); + } + + // Generate new + console.log("Generating new server identity..."); + const signKeys = await generateSigningKeyPair(); // Ed25519 + const encKeys = await generateEncryptionKeyPair(); // X25519 + + identity = { + publicKey: signKeys.publicKey, + privateKey: signKeys.privateKey, + encryptionPublicKey: encKeys.publicKey, + encryptionPrivateKey: encKeys.privateKey, + domain, + }; + + await fs.writeFile(IDENTITY_FILE, JSON.stringify(identity, null, 2)); + return identity; +} + +export async function signServerRequest( + payload: any, +): Promise<{ signature: string; timestamp: number; domain: string }> { + const id = await getServerIdentity(); + const timestamp = Date.now(); + // Deterministic canonical JSON needed? Or just sign payload string/bytes? + // Let's sign: domain + timestamp + JSON.stringify(payload) + const msg = `${id.domain}:${timestamp}:${JSON.stringify(payload)}`; + const sig = await sign(new TextEncoder().encode(msg), id.privateKey); + return { + signature: sig, + timestamp, + domain: id.domain, + }; +} diff --git a/src/lib/server/pubsub.ts b/src/lib/server/pubsub.ts new file mode 100644 index 0000000..d666c80 --- /dev/null +++ b/src/lib/server/pubsub.ts @@ -0,0 +1,22 @@ +import { EventEmitter } from "events"; + +class NotePubSub extends EventEmitter { + constructor() { + super(); + // Increase limit in case of many connections + this.setMaxListeners(1000); + } + + publish(docId: string, data: unknown) { + this.emit(`op:${docId}`, data); + } + + subscribe(docId: string, callback: (data: unknown) => void) { + const eventName = `op:${docId}`; + this.on(eventName, callback); + return () => this.off(eventName, callback); + } +} + +// Singleton instance +export const notePubSub = new NotePubSub(); diff --git a/src/lib/server/real-time.ts b/src/lib/server/real-time.ts index 1e06f31..19c191f 100644 --- a/src/lib/server/real-time.ts +++ b/src/lib/server/real-time.ts @@ -11,7 +11,7 @@ export function addClient( } clients.get(noteId)!.add(controller); - console.debug( + console.log( `Client added to note ${noteId}. Total clients: ${clients.get(noteId)?.size}`, ); } @@ -26,7 +26,7 @@ export function removeClient( if (set.size === 0) { clients.delete(noteId); } - console.debug(`Client removed from note ${noteId}. Remaining: ${set.size}`); + console.log(`Client removed from note ${noteId}. Remaining: ${set.size}`); } } diff --git a/src/lib/utils/time.ts b/src/lib/utils/time.ts new file mode 100644 index 0000000..d78932b --- /dev/null +++ b/src/lib/utils/time.ts @@ -0,0 +1,28 @@ +import { Temporal } from "temporal-polyfill"; + +const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: "auto" }); + +export function formatRelativeTime(then: Temporal.Instant): string { + const now = Temporal.Now.zonedDateTimeISO(); + const thenZoned = then.toZonedDateTimeISO(now.timeZoneId); + + // If before today's midnight, show days ago using Intl + const daysDiff = thenZoned.toPlainDate().until(now.toPlainDate()).days; + if (daysDiff >= 1) { + return rtf.format(-daysDiff, "day"); + } + + // Otherwise show hours/minutes/seconds using Intl + const duration = now.toInstant().since(then, { + largestUnit: "hour", + smallestUnit: "second", + }); + + if (duration.hours >= 1) { + return rtf.format(-duration.hours, "hour"); + } + if (duration.minutes >= 1) { + return rtf.format(-duration.minutes, "minute"); + } + return rtf.format(-duration.seconds, "second"); +} diff --git a/src/routes/(auth)/login/+page.svelte b/src/routes/(auth)/login/+page.svelte index 1b1e2c5..27776e9 100644 --- a/src/routes/(auth)/login/+page.svelte +++ b/src/routes/(auth)/login/+page.svelte @@ -16,7 +16,19 @@
{/each} -
+ { + // Cache password for the layout to use for decryption + if (_password) { + sessionStorage.setItem("notes_temp_password", _password); + } + + // TODO: do we even need this? + await submit(); + })} + >
-
diff --git a/src/routes/(auth)/signup/+page.svelte b/src/routes/(auth)/signup/+page.svelte index 59c81e8..0fa3827 100644 --- a/src/routes/(auth)/signup/+page.svelte +++ b/src/routes/(auth)/signup/+page.svelte @@ -1,11 +1,13 @@
@@ -19,13 +21,14 @@
{/each} - + +
diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index 405e5dd..ce23982 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,16 +1,18 @@ import type { User } from "$lib/schema.ts"; import { db } from "$lib/server/db"; -import { eq } from "drizzle-orm"; +import { eq, ne } from "drizzle-orm"; +import type { SharedNote } from "$lib/remote/notes.remote.ts"; export interface Data { user: User | undefined; + sharedNotes: SharedNote[]; } export const load = async ({ locals }): Promise => { const localUser = locals.user; if (!localUser) { - return { user: undefined }; + return { user: undefined, sharedNotes: [] }; } // Get user with private key from database @@ -18,14 +20,28 @@ export const load = async ({ locals }): Promise => { where: (users) => eq(users.id, localUser.id), }); + // Get shared notes directly from DB + const sharedDocs = await db.query.documents.findMany({ + where: (docs) => ne(docs.hostServer, "local"), + }); + + const sharedNotes: SharedNote[] = sharedDocs.map((doc) => ({ + id: doc.id, + title: doc.title ?? "Untitled", + hostServer: doc.hostServer, + ownerId: doc.ownerId, + accessLevel: doc.accessLevel, + })); + return { user: user ? { id: user.id, username: user.username, - publicKey: user.publicKey, + publicKey: user.publicKey ?? "", // Handle null publicKey privateKeyEncrypted: user.privateKeyEncrypted, } : undefined, + sharedNotes, }; }; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 70dac07..41c8f97 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -3,8 +3,166 @@ import { onNavigate } from "$app/navigation"; import favicon from "$lib/assets/favicon.svg"; + import Sidebar from "$lib/components/Sidebar.svelte"; + import { setSidebarContext } from "$lib/components/sidebar-context.ts"; + import { + decryptWithPassword, + encryptWithPassword, + generateSigningKeyPair, + generateEncryptionKeyPair, + } from "$lib/crypto.ts"; + import { setupEncryption } from "$lib/remote/accounts.remote.ts"; + import { getNotes } from "$lib/remote/notes.remote.ts"; + import { PersistedState } from "runed"; + import { onMount } from "svelte"; + + let { children, data } = $props(); + + // User's explicit preference (persisted to localStorage) + const collapsedDesktop = new PersistedState("sidebarCollapsed", false); + // Mobile state (always starts collapsed) + let collapsedMobile = $state(true); + + // Track window width + let innerWidth = $state(0); + let isDesktop = $derived(innerWidth >= 768); + + // Derived visual state + let isCollapsed = $derived( + isDesktop ? collapsedDesktop.current : collapsedMobile, + ); + + // Handle transitions between breakpoints + let wasDesktop = $state(true); + + $effect(() => { + if (innerWidth === 0) return; + + // Desktop -> Mobile: Always collapse + if (wasDesktop && !isDesktop) { + collapsedMobile = true; + } + + // Mobile -> Desktop: If mobile was open, keep open + if (!wasDesktop && isDesktop) { + if (!collapsedMobile) { + collapsedDesktop.current = false; + } + } + + wasDesktop = isDesktop; + }); + + function toggleSidebar() { + if (isDesktop) { + collapsedDesktop.current = !collapsedDesktop.current; + } else { + collapsedMobile = !collapsedMobile; + } + } + + // Keyboard shortcut: Ctrl+\ (or Cmd+\ on Mac) + function handleKeydown(e: KeyboardEvent) { + if ((e.ctrlKey || e.metaKey) && e.key === "\\") { + e.preventDefault(); + toggleSidebar(); + } + } + + setSidebarContext({ + get isCollapsed() { + return isCollapsed; + }, + toggleSidebar, + }); + + // Vault State + let isVaultUnlocked = $state(false); + let unlockPassword = $state(""); + let unlockError = $state(""); + + // Setup Encryption State + let isSetupRequired = $derived(!!data.user && !data.user.privateKeyEncrypted); + let setupPassword = $state(""); + let setupError = $state(""); + let setupLoading = $state(false); + + async function handleSetupEncryption() { + if (!data.user || !setupPassword) return; + setupLoading = true; + setupError = ""; + + try { + // 1. Generate Keys + const signKeys = await generateSigningKeyPair(); + const encKeys = await generateEncryptionKeyPair(); + + // 2. Encrypt + const privateKeyEncrypted = await encryptWithPassword( + encKeys.privateKey, + setupPassword, + ); + + // 3. Submit + await setupEncryption({ + _password: setupPassword, + publicKey: signKeys.publicKey, + privateKeyEncrypted, + }); + + // 4. Auto-unlock locally + sessionStorage.setItem("notes_raw_private_key", encKeys.privateKey); + isVaultUnlocked = true; + + // Reload to ensure state is fresh + window.location.reload(); + } catch (e) { + console.error(e); + setupError = "Failed to setup encryption. Verify your password."; + } finally { + setupLoading = false; + } + } + + // Global Private Key State (exposed via Context?) + // For now, we rely on sessionStorage "notes_raw_private_key" being present. + + function unlockVault() { + if (!unlockPassword || !data.user) return; + try { + const rawKey = decryptWithPassword( + data.user.privateKeyEncrypted, + unlockPassword, + ); + sessionStorage.setItem("notes_raw_private_key", rawKey); + isVaultUnlocked = true; + unlockPassword = ""; // clear memory + } catch { + unlockError = "Incorrect password"; + } + } + + const notesList = $derived(data.user ? await getNotes() : []); + + // Initialize from localStorage and handle responsive behavior + onMount(() => { + if (data.user) { + // Try to auto-unlock if key is already in session + const existingKey = sessionStorage.getItem("notes_raw_private_key"); + if (existingKey) { + isVaultUnlocked = true; + } else { + // Try temporary password from login redirect + const tempPw = sessionStorage.getItem("notes_temp_password"); + if (tempPw) { + unlockPassword = tempPw; + unlockVault(); + sessionStorage.removeItem("notes_temp_password"); + } + } + } + }); - let { children } = $props(); onNavigate((navigation) => { const { promise, resolve } = Promise.withResolvers(); @@ -17,8 +175,103 @@ }); + + -{@render children()} +{#if data.user} + {#if isSetupRequired} +
+
+
+

Setup Encryption

+ +

+ Your account needs to be upgraded to support End-to-End Encryption. + Please confirm your password to generate your secure keys. +

+ + {#if setupError} +
{setupError}
+ {/if} + +
+ e.key === "Enter" && handleSetupEncryption()} + disabled={setupLoading} + /> +
+ +
+ + + + +
+
+
+
+ {:else if !isVaultUnlocked} +
+
+
+

Unlock Your Vault

+

Enter your password to decrypt your private key.

+ {#if unlockError} +
{unlockError}
+ {/if} +
+ e.key === "Enter" && unlockVault()} + /> +
+
+ + +
+ +
+
+
+
+
+ {:else} +
+ +
+ {@render children()} +
+
+ {/if} +{:else} + {@render children()} +{/if} diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 7d2c9b0..ead66ba 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,10 +1,15 @@ -
+

Dashboard

@@ -31,9 +36,11 @@ {data.randomNote.title}

- Last updated: {new Date( - data.randomNote.updatedAt, - ).toLocaleDateString()} + Last updated: {formatRelativeTime( + Temporal.Instant.fromEpochMilliseconds( + data.randomNote.updatedAt.getTime(), + ), + )}

@@ -57,9 +64,19 @@

- Create New Note + {#if sidebar.isCollapsed} + + {:else} +

+ Use the sidebar to create your first note +

+ {/if}
diff --git a/src/routes/.well-known/notes-identity/[handle]/+server.ts b/src/routes/.well-known/notes-identity/[handle]/+server.ts new file mode 100644 index 0000000..8714951 --- /dev/null +++ b/src/routes/.well-known/notes-identity/[handle]/+server.ts @@ -0,0 +1,41 @@ +import { json } from "@sveltejs/kit"; +import { db } from "$lib/server/db"; +import { users } from "$lib/server/db/schema"; +import { eq } from "drizzle-orm"; +import { env } from "$env/dynamic/private"; + +export async function GET({ params }) { + const { handle } = params; + + // Assume handle is the username for now for local lookups + // Ideally this endpoint serves `/.well-known/notes-identity/@alice` + // so we strip the @ if needed, or query by it. + + // Clean handle: remove leading @ + const cleanHandle = handle.startsWith("@") ? handle.slice(1) : handle; + + // If handle contains ':', it might include domain, but this is the home server, + // so we expect to serve our own users. + // We'll look up by username locally. + const username = cleanHandle.split(":")[0]; + + const user = await db.query.users.findFirst({ + where: eq(users.username, username), + }); + + if (!user) { + return new Response("Not found", { status: 404 }); + } + + // Return public identity + // IMPORTANT: Return the FULL federated handle so other servers know exactly who this is. + // e.g. @bob -> @bob:localhost:5174 + const fullHandle = `@${user.username}:${env["SERVER_DOMAIN"] ?? "localhost:5173"}`; + + return json({ + id: user.id, + handle: fullHandle, + publicKey: user.publicKey, + devices: [], // TODO: fetch devices + }); +} diff --git a/src/routes/.well-known/notes-server/+server.ts b/src/routes/.well-known/notes-server/+server.ts new file mode 100644 index 0000000..a66822c --- /dev/null +++ b/src/routes/.well-known/notes-server/+server.ts @@ -0,0 +1,10 @@ +import { json } from "@sveltejs/kit"; +import { getServerIdentity } from "$lib/server/identity"; + +export async function GET() { + const id = await getServerIdentity(); + return json({ + domain: id.domain, + publicKey: id.publicKey, + }); +} diff --git a/src/routes/api/notes/[id]/leave/+server.ts b/src/routes/api/notes/[id]/leave/+server.ts new file mode 100644 index 0000000..ed093d0 --- /dev/null +++ b/src/routes/api/notes/[id]/leave/+server.ts @@ -0,0 +1,62 @@ +import { json, error } from "@sveltejs/kit"; +import { db } from "$lib/server/db"; +import { notes, noteShares, members, documents } from "$lib/server/db/schema"; +import { eq, and } from "drizzle-orm"; +import { requireLogin } from "$lib/server/auth"; + +/** + * Leave API endpoint + * + * POST: Leave a note (remove self as member) + */ + +export async function POST({ params }) { + const { user } = requireLogin(); + const { id: noteId } = params; + + // Find the note + const note = await db.query.notes.findFirst({ + where: eq(notes.id, noteId), + }); + + if (!note) { + error(404, "Note not found"); + } + + // Can't leave if you're the owner + if (note.ownerId === user.id) { + error( + 400, + "Owner cannot leave their own note. Transfer ownership or delete the note instead.", + ); + } + + // Remove self from noteShares + await db + .delete(noteShares) + .where( + and( + eq(noteShares.noteId, noteId), + eq(noteShares.sharedWithUser, user.id), + ), + ); + + // Remove self from members table (federation) + await db + .delete(members) + .where(and(eq(members.docId, noteId), eq(members.userId, user.id))); + + // Delete local copy of the note if it's a federated note we don't own + // Check if this is a federated note by looking at the documents table + const doc = await db.query.documents.findFirst({ + where: eq(documents.id, noteId), + }); + + if (doc && doc.hostServer !== "local") { + // This is a federated note - delete our local copy + await db.delete(notes).where(eq(notes.id, noteId)); + await db.delete(documents).where(eq(documents.id, noteId)); + } + + return json({ success: true, leftNoteId: noteId }); +} diff --git a/src/routes/api/notes/[id]/members/+server.ts b/src/routes/api/notes/[id]/members/+server.ts new file mode 100644 index 0000000..2ebe3ee --- /dev/null +++ b/src/routes/api/notes/[id]/members/+server.ts @@ -0,0 +1,179 @@ +import { json, error } from "@sveltejs/kit"; +import { db } from "$lib/server/db"; +import { notes, noteShares, members } from "$lib/server/db/schema"; +import { eq, and } from "drizzle-orm"; +import { requireLogin } from "$lib/server/auth"; + +/** + * Members API endpoint + * + * GET: Get list of members for a note + * POST: Add a member to the note (owner only) + * DELETE: Remove a member from the note (owner only) + */ + +export interface Member { + userId: string; // Federated handle or local user ID + role: string; // owner, writer, reader + addedAt?: string; // When they were added +} + +// GET members list +export async function GET({ params }) { + const { user } = requireLogin(); + const { id: noteId } = params; + + const note = await db.query.notes.findFirst({ + where: eq(notes.id, noteId), + }); + + if (!note) { + error(404, "Note not found"); + } + + // Check if user has access (owner or member) + const isOwner = note.ownerId === user.id; + + // Get shares from noteShares table (for invite_only mode) + const shares = await db.query.noteShares.findMany({ + where: eq(noteShares.noteId, noteId), + }); + + // Get members from members table (for federation) + const membersList = await db.query.members.findMany({ + where: eq(members.docId, noteId), + }); + + // Build combined member list + const result: Member[] = []; + + // Add owner first + result.push({ + userId: note.ownerId, + role: "owner", + }); + + // Add invited users from noteShares + for (const share of shares) { + if (share.sharedWithUser !== note.ownerId) { + result.push({ + userId: share.sharedWithUser, + role: share.permissions === "write" ? "writer" : "reader", + addedAt: share.createdAt.toISOString(), + }); + } + } + + // Add federated members + for (const member of membersList) { + // Avoid duplicates + if (!result.find((m) => m.userId === member.userId)) { + result.push({ + userId: member.userId, + role: member.role, + addedAt: member.createdAt.toISOString(), + }); + } + } + + return json({ + noteId, + isOwner, + accessLevel: note.accessLevel, + members: result, + }); +} + +// POST add member +export async function POST({ params, request }) { + const { user } = requireLogin(); + const { id: noteId } = params; + + const body = await request.json(); + const { userId, role = "writer" } = body; + + if (!userId) { + error(400, "userId is required"); + } + + // Find the note and verify ownership + const note = await db.query.notes.findFirst({ + where: eq(notes.id, noteId), + }); + + if (!note) { + error(404, "Note not found"); + } + + if (note.ownerId !== user.id) { + error(403, "Only the owner can add members"); + } + + // Add to noteShares for invite_only mode + const shareId = crypto.randomUUID(); + await db + .insert(noteShares) + .values({ + id: shareId, + noteId, + sharedWithUser: userId, + encryptedKey: "", // Will be populated when they request access + permissions: role === "reader" ? "read" : "write", + createdAt: new Date(), + }) + .onConflictDoNothing(); + + // If invite_only mode is not set, set it + if (note.accessLevel === "private") { + await db + .update(notes) + .set({ accessLevel: "invite_only", updatedAt: new Date() }) + .where(eq(notes.id, noteId)); + } + + return json({ success: true, userId, role }); +} + +// DELETE remove member +export async function DELETE({ params, request }) { + const { user } = requireLogin(); + const { id: noteId } = params; + + const url = new URL(request.url); + const userId = url.searchParams.get("userId"); + + if (!userId) { + error(400, "userId query parameter is required"); + } + + // Find the note and verify ownership + const note = await db.query.notes.findFirst({ + where: eq(notes.id, noteId), + }); + + if (!note) { + error(404, "Note not found"); + } + + if (note.ownerId !== user.id) { + error(403, "Only the owner can remove members"); + } + + if (userId === note.ownerId) { + error(400, "Cannot remove the owner"); + } + + // Remove from noteShares + await db + .delete(noteShares) + .where( + and(eq(noteShares.noteId, noteId), eq(noteShares.sharedWithUser, userId)), + ); + + // Remove from members table (federation) + await db + .delete(members) + .where(and(eq(members.docId, noteId), eq(members.userId, userId))); + + return json({ success: true, removedUserId: userId }); +} diff --git a/src/routes/api/notes/[id]/share/+server.ts b/src/routes/api/notes/[id]/share/+server.ts new file mode 100644 index 0000000..270e9f9 --- /dev/null +++ b/src/routes/api/notes/[id]/share/+server.ts @@ -0,0 +1,213 @@ +import { json, error } from "@sveltejs/kit"; +import { db } from "$lib/server/db"; +import { notes, noteShares, members, documents } from "$lib/server/db/schema"; +import { eq } from "drizzle-orm"; +import { requireLogin } from "$lib/server/auth"; +import { env } from "$env/dynamic/private"; +import { + fetchUserIdentity, + encryptDocumentKeyForUser, +} from "$lib/server/federation"; + +/** + * Share API endpoint + * + * POST: Update sharing settings for a note + * GET: Get current sharing settings + */ + +export interface ShareSettings { + accessLevel: + | "private" + | "invite_only" + | "authenticated" + | "open" + | "password_protected"; + invitedUsers?: string[]; // Federated handles like @user:domain.com + passwordEncryptedKey?: string; // Encrypted with the password +} + +// GET current share settings +export async function GET({ params }) { + const { user } = requireLogin(); + const { id: noteId } = params; + + const note = await db.query.notes.findFirst({ + where: eq(notes.id, noteId), + }); + + if (!note) { + error(404, "Note not found"); + } + + if (note.ownerId !== user.id) { + error(403, "Only the owner can view share settings"); + } + + // Get invited users for this note + const shares = await db.query.noteShares.findMany({ + where: eq(noteShares.noteId, noteId), + }); + + return json({ + accessLevel: note.accessLevel || "private", + invitedUsers: shares.map((s) => s.sharedWithUser), + // We do NOT return the passwordEncryptedKey to the owner here, they don't need it (they have the original key) + }); +} + +// POST update share settings +export async function POST({ params, request }) { + const { user } = requireLogin(); + const { id: noteId } = params; + + const body = await request.json(); + const { accessLevel, invitedUsers, passwordEncryptedKey } = + body as ShareSettings; + + // Validate access level + if ( + ![ + "private", + "invite_only", + "authenticated", + "open", + "password_protected", + ].includes(accessLevel) + ) { + error(400, "Invalid access level"); + } + + // Find the note + const note = await db.query.notes.findFirst({ + where: eq(notes.id, noteId), + }); + + if (!note) { + error(404, "Note not found"); + } + + if (note.ownerId !== user.id) { + error(403, "Only the owner can update share settings"); + } + + // Get the document key (encrypted for owner) + const encryptedDocKey = note.documentKeyEncrypted ?? note.encryptedKey; + const serverDomain = env["SERVER_DOMAIN"] ?? "localhost:5173"; + + // Update note access level + await db + .update(notes) + .set({ + accessLevel, + updatedAt: new Date(), + }) + .where(eq(notes.id, noteId)); + + // Track failed invites for response + const failedInvites: string[] = []; + const successfulInvites: string[] = []; + + // Handle invited users for invite_only mode + if ( + accessLevel === "invite_only" && + invitedUsers && + invitedUsers.length > 0 + ) { + // Clear existing shares (we'll re-add them) + await db.delete(noteShares).where(eq(noteShares.noteId, noteId)); + + // Add new shares with encrypted keys + for (const userHandle of invitedUsers) { + const shareId = crypto.randomUUID(); + let encryptedKey = ""; + + // Try to fetch user's public key and encrypt document key + try { + console.log(`[SHARE] Fetching identity for ${userHandle}...`); + const identity = await fetchUserIdentity(userHandle, serverDomain); + + if (identity) { + console.log( + `[SHARE] Found identity for ${userHandle}:`, + identity.handle, + ); + const encrypted = encryptDocumentKeyForUser( + encryptedDocKey, + identity, + ); + + if (encrypted) { + encryptedKey = encrypted; + successfulInvites.push(identity.handle); // Use canonical handle + + // Also add to members table for federation + await db + .insert(members) + .values({ + docId: noteId, + userId: identity.handle, // Canonical handle + deviceId: "primary", + role: "writer", + encryptedKeyEnvelope: encryptedKey, + createdAt: new Date(), + }) + .onConflictDoNothing(); + } else { + console.error(`[SHARE] Failed to encrypt key for ${userHandle}`); + failedInvites.push(userHandle); + } + } else { + // User not found - still add share, key will be generated on join + console.warn(`[SHARE] User not found: ${userHandle}`); + failedInvites.push(userHandle); + } + } catch (err) { + console.error( + `[SHARE] Error processing invite for ${userHandle}:`, + err, + ); + failedInvites.push(userHandle); + } + + // Always store the share record (even if key encryption failed) + await db.insert(noteShares).values({ + id: shareId, + noteId, + sharedWithUser: userHandle, + encryptedKey, + permissions: "write", + createdAt: new Date(), + }); + } + } else if (accessLevel !== "invite_only") { + // Clear invited users if not in invite_only mode + await db.delete(noteShares).where(eq(noteShares.noteId, noteId)); + await db.delete(members).where(eq(members.docId, noteId)); + } + + // Also update the documents table if it exists (for federation) + const doc = await db.query.documents.findFirst({ + where: eq(documents.id, noteId), + }); + + if (doc) { + await db + .update(documents) + .set({ + accessLevel, + // Only update if provided (don't overwrite with undefined) + ...(passwordEncryptedKey ? { passwordEncryptedKey } : {}), + updatedAt: new Date(), + }) + .where(eq(documents.id, noteId)); + } + + return json({ + success: true, + accessLevel, + invitedUsers: invitedUsers ?? [], + successfulInvites, + failedInvites, + }); +} diff --git a/src/routes/api/server-identity/+server.ts b/src/routes/api/server-identity/+server.ts new file mode 100644 index 0000000..a5266dc --- /dev/null +++ b/src/routes/api/server-identity/+server.ts @@ -0,0 +1,12 @@ +import { json } from "@sveltejs/kit"; +import { getServerIdentity } from "$lib/server/identity"; + +export async function GET() { + const identity = await getServerIdentity(); + + return json({ + domain: identity.domain, + publicKey: identity.publicKey, // Signing + encryptionPublicKey: identity.encryptionPublicKey, // Broker + }); +} diff --git a/src/routes/api/sync/[noteId]/+server.ts b/src/routes/api/sync/[noteId]/+server.ts index 1d70aeb..9c794a9 100644 --- a/src/routes/api/sync/[noteId]/+server.ts +++ b/src/routes/api/sync/[noteId]/+server.ts @@ -1,12 +1,12 @@ -import { syncSchemaJson } from "$lib/remote/notes.schemas.ts"; import { db } from "$lib/server/db"; import { notes } from "$lib/server/db/schema"; import { addClient, removeClient } from "$lib/server/real-time"; import { json } from "@sveltejs/kit"; import { eq } from "drizzle-orm"; -import { Schema } from "effect"; export const GET = async ({ params, locals }) => { + console.log("SSE connection request for note:", params.noteId); + if (!locals.user) { return json({ error: "Unauthorized" }, { status: 401 }); } @@ -28,19 +28,28 @@ export const GET = async ({ params, locals }) => { // Create a stream for SSE let controller: ReadableStreamDefaultController>; + let keepAliveInterval: NodeJS.Timeout; + const stream = new ReadableStream>({ start(c) { controller = c; addClient(noteId, controller); + // Send initial connection message const encoder = new TextEncoder(); - c.enqueue( - encoder.encode( - `event: connected\ndata: ${Schema.encodeSync(syncSchemaJson)({ noteId, updates: [] })}\n\n`, - ), - ); + c.enqueue(encoder.encode(`event: connected\n`)); + + // Send keep-alive comment every 15 seconds to prevent timeout + keepAliveInterval = setInterval(() => { + try { + c.enqueue(encoder.encode(`: keep-alive\n\n`)); + } catch { + clearInterval(keepAliveInterval); + } + }, 15000); }, cancel() { + clearInterval(keepAliveInterval); removeClient(noteId, controller); }, }); diff --git a/src/routes/client/doc/[doc_id]/events/+server.ts b/src/routes/client/doc/[doc_id]/events/+server.ts new file mode 100644 index 0000000..a1f0853 --- /dev/null +++ b/src/routes/client/doc/[doc_id]/events/+server.ts @@ -0,0 +1,189 @@ +import { db } from "$lib/server/db"; +import { federatedOps, documents } from "$lib/server/db/schema"; +import { eq, gt, asc, and } from "drizzle-orm"; +import type { RequestHandler } from "./$types"; +import { error } from "@sveltejs/kit"; +import { notePubSub } from "$lib/server/pubsub"; +import { parseNoteId } from "$lib/noteId"; +import { unawaited } from "$lib/unawaited.ts"; + +export const GET: RequestHandler = async ({ params: { doc_id }, url }) => { + console.log("HIT-SSE: Handler invoked for", doc_id); + const since = url.searchParams.get("since"); + // Default to 0 (beginning of time) to fetch full history if 'since' is not provided. + const lastTs = since ? parseInt(since) : 0; + + console.log( + `[EVENTS] Connection request for ${doc_id}, since=${since ?? "0"}`, + ); + + let doc: { hostServer: string } | undefined = + await db.query.documents.findFirst({ + where: eq(documents.id, doc_id), + }); + + // If not in DB, check if it's a valid remote ID (Ephemeral/Anonymous access) + if (!doc) { + try { + const { origin } = parseNoteId(doc_id); + + if (origin) { + console.log( + `[EVENTS] Ephemeral note inferred from ID. Origin: ${origin}. Proxying...`, + ); + doc = { hostServer: origin }; + } + } catch (e) { + console.error("[EVENTS] ERROR parsing note ID:", e); + // Fallthrough to 404 + } + } + + if (!doc) { + console.error(`[EVENTS] Document not found: ${doc_id}`); + error(404, "Document not found"); + } + + const isRemote = doc.hostServer !== "local"; + + let remoteReader: ReadableStreamDefaultReader | null = null; + let abortController: AbortController | null = null; + let heartbeat: NodeJS.Timeout | null = null; + let unsubscribe: (() => void) | null = null; + + const stream = new ReadableStream({ + async start(controller) { + try { + if (isRemote) { + console.log(`[CLIENT-SSE] Proxying to remote ${doc.hostServer}`); + + // Connect to remote SSE + const remoteUrl = `http://${doc.hostServer}/federation/doc/${encodeURIComponent(doc_id)}/events?since=${lastTs.toFixed()}`; + + // We need to sign this if we enforce auth, but we relaxed it for now. + abortController = new AbortController(); + const response = await fetch(remoteUrl, { + headers: { + Accept: "text/event-stream", + }, + signal: abortController.signal, + }); + + if (!response.ok || !response.body) { + console.error( + `[CLIENT-SSE] Remote connection failed:`, + response.status, + ); + throw new Error(`Remote SSE failed: ${response.status.toFixed()}`); + } + + remoteReader = response.body.getReader(); + + // Pipe remote stream to local controller + // Combine with local PubSub to get instant updates from other local users + unsubscribe = notePubSub.subscribe(doc_id, (newOps) => { + try { + controller.enqueue(`data: ${JSON.stringify(newOps)}\n\n`); + } catch (e) { + console.warn( + "[CLIENT-SSE] Failed to enqueue local op (stream closed?):", + e, + ); + } + }); + + // Pipe remote stream to local controller + // This must be awaited to keep the stream open + try { + while (true) { + const { done, value } = await remoteReader.read(); + if (done) break; + controller.enqueue(value); + } + } catch (e) { + console.error("[CLIENT-SSE] Remote stream error:", e); + controller.error(e); + } finally { + // Ensure we unsubscribe from local updates when remote stream ends or errors + unsubscribe(); + } + } else { + // Local logic: Monitor DB (Polling or PubSub locally too?) + // For local, we can ALSO use PubSub! + // But 'events/+server.ts' is used by the client. + // IF we use PubSub locally, we get instant updates for local users too (multi-tab/window). + + // notePubSub is imported statically + // 1. Initial history + const ops = await db.query.federatedOps.findMany({ + where: and( + eq(federatedOps.docId, doc_id), + gt(federatedOps.lamportTs, lastTs), + ), + orderBy: [asc(federatedOps.lamportTs)], + }); + + if (ops.length > 0) { + controller.enqueue(`data: ${JSON.stringify(ops)}\n\n`); + } + + // 2. Subscribe + unsubscribe = notePubSub.subscribe(doc_id, (newOps) => { + try { + controller.enqueue(`data: ${JSON.stringify(newOps)}\n\n`); + } catch (e) { + console.warn( + "[CLIENT-SSE] Failed to enqueue (stream closed?):", + e, + ); + // Unsubscribe to prevent future errors? + // The controller.close() or cancel should have triggered cleanup, + // but if we are here, maybe it didn't yet. + } + }); + + // Keep alive + heartbeat = setInterval(() => { + try { + controller.enqueue(": keep-alive\n\n"); + } catch { + if (heartbeat) clearInterval(heartbeat); + } + }, 30000); + + // Never resolve, keep stream open until cancelled + await new Promise(() => { + /* empty */ + }); + } + } catch (e) { + console.error("[CLIENT-SSE] Stream error:", e); + // If error occurs, we should cleanup and close + if (remoteReader) unawaited(remoteReader.cancel()); + if (abortController) abortController.abort(); + if (heartbeat) clearInterval(heartbeat); + if (unsubscribe) unsubscribe(); + try { + controller.close(); + } catch { + // ignore if already closed + } + } + }, + cancel() { + console.log(`[CLIENT-SSE] Stream cancelled for ${doc_id}`); + if (remoteReader) unawaited(remoteReader.cancel()); + if (abortController) abortController.abort(); + if (heartbeat) clearInterval(heartbeat); + if (unsubscribe) unsubscribe(); + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); +}; diff --git a/src/routes/client/doc/[doc_id]/push/+server.ts b/src/routes/client/doc/[doc_id]/push/+server.ts new file mode 100644 index 0000000..801a6b7 --- /dev/null +++ b/src/routes/client/doc/[doc_id]/push/+server.ts @@ -0,0 +1,161 @@ +import { json, error } from "@sveltejs/kit"; +import { db } from "$lib/server/db"; +import { federatedOps, documents } from "$lib/server/db/schema"; +import { eq } from "drizzle-orm"; +import { signServerRequest } from "$lib/server/identity"; +import { notePubSub } from "$lib/server/pubsub"; +import { parseNoteId } from "$lib/noteId"; + +export async function POST({ params, request, locals }) { + const { doc_id } = params; + const body = await request.json(); + const { op } = body; + + let doc = await db.query.documents.findFirst({ + where: eq(documents.id, doc_id), + }); + + // Handle ephemeral/anonymous notes (not in DB) + if (!doc) { + try { + const { origin } = parseNoteId(doc_id); + if (origin) { + console.log(`[CLIENT] Infers ephemeral note origin: ${origin}`); + doc = { hostServer: origin, accessLevel: "public" } as any; + } + } catch (e) { + console.warn("Failed to parse ephemeral ID for push:", e); + } + } + + // Permission Check + if (!doc) { + error(404, "Note not found"); + } + + // If private and not logged in, deny + if (doc.accessLevel === "private" && !locals.user) { + error(401, "Unauthorized"); + } + // TODO: fine-grained auth for 'mixed' mode (e.g. public read, private write) + // For now, if it's 'public' or 'open', we allow anonymous writes. + + if (doc.hostServer !== "local") { + // Proxy to remote server + console.log( + `[CLIENT] Proxying push to remote server: ${doc.hostServer} for ${doc_id}`, + ); + + const remoteUrl = `http://${doc.hostServer}/federation/doc/${encodeURIComponent(doc_id)}/ops`; + const payload = { ops: [op] }; + + console.log(`[CLIENT] Signing request...`); + // We sign as the SERVER, not the user. + const { + signature, + timestamp, + domain: requestDomain, + } = await signServerRequest(payload); + + console.log(`[CLIENT] Sending fetch to ${remoteUrl}`); + const res = await fetch(remoteUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-notes-signature": signature, + "x-notes-timestamp": timestamp.toString(), + "x-notes-domain": requestDomain, + }, + body: JSON.stringify(payload), + }); + + console.log(`[CLIENT] Remote response status: ${res.status}`); + if (!res.ok) { + // If remote 401s, we 401? + error(res.status, "Remote push failed"); + } + } + + // Store Op locally ONLY if we have a real local document record + // Ephemeral notes (doc.hostServer inferred but not in DB) cannot store ops locally due to FK. + // We check if the doc was actually found in DB. + // We can check if 'createdAt' exists or similar, but cleaner is to re-check specific flag or use original query result. + // Actually, 'doc' is mutated above. + // Let's rely on checking if it exists in DB. + + // Re-query or check if it has an ID/real fields? + // The 'doc' variable might be our mock object. + // Check if we can just generic 'try/catch' the insert. + + try { + // Check if doc exists in DB using a quick query or assume from context. + // If we just pushed to remote, we might be done. + // But if we are the HOST, we MUST insert. + // If doc.hostServer is local, then 'doc' MUST be from DB (since we can't infer local origin from ID for *new* notes without DB record usually, unless we are being hacked). + // Actually, if !doc, we only inferred if origin != null. + + // Simplest: Try insert. If FK violation, ignore? + // But we don't want to throw 500. + + // Only insert if we think it's in the DB. + // Since we don't distinguish easily with the 'doc' var reuse, let's just attempt insert and catch specific error, or only insert if hostServer is local? + // No, we cache remote ops too IF we have the doc stub. + + // Better: Helper variable 'existsInDb'. + const existsInDb = !!(await db.query.documents.findFirst({ + where: eq(documents.id, doc_id), + columns: { id: true }, + })); + + if (existsInDb) { + console.log( + `[CLIENT] Inserting op ${op.op_id} into federatedOps (docId: ${doc_id})`, + ); + const normalizedOp = { + id: op.op_id, + docId: doc_id, + opId: op.op_id, + actorId: op.actor_id, + lamportTs: op.lamport_ts, + payload: op.encrypted_payload, + signature: op.signature, + createdAt: new Date(), + }; + + await db.insert(federatedOps).values(normalizedOp).onConflictDoNothing(); + console.log(`[CLIENT] Local insertion successful for ${op.op_id}`); + + // Publish to PubSub + notePubSub.publish(doc_id, [normalizedOp]); + } else { + console.log( + `[CLIENT] Skipping local storage for ephemeral/remote note ${doc_id}`, + ); + // But we DO need to publish to PubSub so the Client SSE (which is listening) gets the echo back! + // The Client SSE subscribes to notePubSub. + // So anonymous user sees their own change reflected immediately? + // Or do they wait for remote roundtrip? + // If we proxy, the remote executes. + // Does the remote send it back via SSE? + // Yes, if we are subscribed. + + // Optimistic local update via PubSub even if not in DB? + // notePubSub is memory-based. So YES/safe. + notePubSub.publish(doc_id, [ + { + ...op, + payload: op.encrypted_payload, // map for client compatibility + }, + ]); + } + } catch (err) { + console.error(`[CLIENT] Local insertion failed for ${op.op_id}:`, err); + // If we successfully proxied, maybe don't fail the whole request? + // If local, we must fail. + if (doc.hostServer === "local") { + error(500, "Failed to store operation locally"); + } + } + + return json({ success: true }); +} diff --git a/src/routes/federation/doc/[doc_id]/events/+server.ts b/src/routes/federation/doc/[doc_id]/events/+server.ts new file mode 100644 index 0000000..2bff1ed --- /dev/null +++ b/src/routes/federation/doc/[doc_id]/events/+server.ts @@ -0,0 +1,79 @@ +import { db } from "$lib/server/db"; +import { federatedOps } from "$lib/server/db/schema"; +import { eq, gt, asc, and } from "drizzle-orm"; +import { notePubSub } from "$lib/server/pubsub"; +import type { RequestHandler } from "./$types"; + +export const GET: RequestHandler = ({ params, url }) => { + const { doc_id } = params; + const since = url.searchParams.get("since"); + const sinceTs = since ? parseInt(since) : 0; + + console.log(`[FED-SSE] Connection for ${doc_id}, since=${sinceTs}`); + + // await verifyServerRequest(request); + + let unsubscribe: (() => void) | undefined; + let interval: NodeJS.Timeout | undefined; + + const stream = new ReadableStream({ + async start(controller) { + // 1. Send History + const ops = await db.query.federatedOps.findMany({ + where: and( + eq(federatedOps.docId, doc_id), + gt(federatedOps.lamportTs, sinceTs), + ), + orderBy: [asc(federatedOps.lamportTs)], + }); + + if (ops.length > 0) { + console.log(`[FED-SSE] Sending ${ops.length} historical ops`); + controller.enqueue(`data: ${JSON.stringify(ops)}\n\n`); + } + + // 2. Subscribe to real-time updates + unsubscribe = notePubSub.subscribe(doc_id, (newOps) => { + try { + controller.enqueue(`data: ${JSON.stringify(newOps)}\n\n`); + } catch (e) { + // If we can't write, the stream is likely dead + console.warn("[FED-SSE] Failed to enqueue (stream closed?):", e); + if (unsubscribe) unsubscribe(); + if (interval) clearInterval(interval); + } + }); + + // Heartbeat to keep connection alive + interval = setInterval(() => { + try { + controller.enqueue(": keep-alive\n\n"); + } catch (e) { + console.warn("[FED-SSE] Heartbeat failed:", e); + if (interval) clearInterval(interval); + if (unsubscribe) unsubscribe(); + } + }, 30000); + + try { + await new Promise(() => { + // Keep stream open forever + }); + } catch { + // Ignored + } + }, + cancel() { + if (unsubscribe) unsubscribe(); + if (interval) clearInterval(interval); + }, + }); + + return new Response(stream, { + headers: { + "Content-Type": "text/event-stream", + "Cache-Control": "no-cache", + Connection: "keep-alive", + }, + }); +}; diff --git a/src/routes/federation/doc/[doc_id]/join/+server.ts b/src/routes/federation/doc/[doc_id]/join/+server.ts new file mode 100644 index 0000000..ae966ca --- /dev/null +++ b/src/routes/federation/doc/[doc_id]/join/+server.ts @@ -0,0 +1,297 @@ +import { json, error } from "@sveltejs/kit"; +import { getServerIdentity } from "$lib/server/identity"; +import { verify, decryptKeyForDevice } from "$lib/crypto"; +import { db } from "$lib/server/db"; +import { documents, members, notes, users } from "$lib/server/db/schema"; +import { eq, and, inArray } from "drizzle-orm"; +import { + fetchUserIdentity, + generateKeyEnvelopesForUsers, +} from "$lib/server/federation"; + +// Helper to verify request signature +async function verifyServerRequest(request: Request, payload: any) { + const signature = request.headers.get("x-notes-signature"); + const timestamp = request.headers.get("x-notes-timestamp"); + const domain = request.headers.get("x-notes-domain"); + + if (!signature || !timestamp || !domain) { + error(401, "Missing signature headers"); + } + + // Fetch remote server key + // In production, we would cache this or use a more robust discovery + // For local dev, we might assume http? Or https with ignore cert? + const protocol = domain.includes("localhost") ? "http" : "https"; + const remoteKeyUrl = `${protocol}://${domain}/.well-known/notes-server`; + + try { + const res = await fetch(remoteKeyUrl); + if (!res.ok) throw new Error("Failed to fetch server key"); + const data = await res.json(); + if (data.domain !== domain) throw new Error("Domain mismatch"); + + const msg = `${domain}:${timestamp}:${JSON.stringify(payload)}`; + const valid = await verify( + signature, + new TextEncoder().encode(msg), + data.publicKey, + ); + + if (!valid) error(401, "Invalid signature"); + return data; // validated server info + } catch (e) { + console.error("Verification failed", e); + error(401, "Verification failed"); + } +} + +export async function POST({ params, request }) { + const { doc_id } = params; + console.log("=== JOIN ENDPOINT START ==="); + console.log(" doc_id from params:", doc_id); + console.log(" decoded doc_id:", decodeURIComponent(doc_id)); + + const body = await request.json(); + const { requesting_server, users: joiningUsers } = body; + console.log(" requesting_server:", requesting_server); + console.log(" joiningUsers:", joiningUsers); + + // Verify signature + const remoteServer = await verifyServerRequest(request, body); + console.log(" remoteServer verified:", remoteServer?.domain); + + // 1. Check if doc exists + // The doc_id MUST be a full portable ID (e.g., bG9jYWxob3N0OjUxNzM~uuid) + console.log(" Searching for doc_id:", doc_id); + + // Try with decoded doc_id (in case it was URL-encoded) + const decodedDocId = decodeURIComponent(doc_id); + + const doc = await db.query.documents.findFirst({ + where: eq(documents.id, decodedDocId), + }); + console.log(" documents.findFirst(decoded):", doc?.id ?? "NOT FOUND"); + + const note = await db.query.notes.findFirst({ + where: eq(notes.id, decodedDocId), + }); + console.log(" notes.findFirst(decoded):", note?.id ?? "NOT FOUND"); + + if (!note && !doc) { + // List all notes in DB for debugging + const allNotes = await db.query.notes.findMany({ limit: 5 }); + console.log( + " All notes in DB (first 5):", + allNotes.map((n) => n.id), + ); + console.error(` Document not found: ${doc_id}`); + error(404, "Document not found"); + } + + console.log(" Found note:", note?.id, "accessLevel:", note?.accessLevel); + + // 2. Check permissions based on access_level + const accessLevel = note?.accessLevel ?? doc?.accessLevel ?? "private"; + + if (accessLevel === "private" || accessLevel === "invite_only") { + // Require pre-existing membership for private/invite-only notes + const memberRows = await db.query.members.findMany({ + where: and( + eq(members.docId, decodedDocId), + inArray(members.userId, joiningUsers), + ), + }); + + if (memberRows.length === 0) { + error(403, "This note is private. You must be invited to access it."); + } + // Permission granted! Fall through to generate fresh envelopes. + } + + // 3. For authenticated/open notes, generate encrypted keys for joining users + const snapshot = note?.loroSnapshot ?? undefined; + const encryptedDocKey = note?.documentKeyEncrypted ?? note?.encryptedKey; + + if (!encryptedDocKey) { + error(500, "Document key not found"); + } + + // Get the owner's private key to decrypt the document key + // Note: In a real E2EE system, the server wouldn't have access to decrypted keys + // This is a simplified approach where the server can re-encrypt for new users + const owner = await db.query.users.findFirst({ + where: eq(users.id, note.ownerId), + }); + + if (!owner) { + error(500, "Document owner not found"); + } + + // For authenticated/open notes, we'll generate envelopes by: + // 1. Fetching user public keys from requesting_server + // 2. Encrypting the document key for each user + + const serverIdentity = await getServerIdentity(); + + // 4. Try to use Server Escrow (Key Broker) + let rawDocKey = ""; + + // Special Handling for Open Public Notes + if (accessLevel === "public" || accessLevel === "open") { + console.log( + "[JOIN] Public Note Access. Decrypting for anonymous access...", + ); + if (doc?.serverEncryptedKey) { + try { + rawDocKey = await decryptKeyForDevice( + doc.serverEncryptedKey, + serverIdentity.encryptionPrivateKey, + ); + } catch (e) { + console.error("[JOIN] Failed to decrypt public note key:", e); + error(500, "Failed to unlock public note"); + } + } else if (encryptedDocKey.length <= 44) { + rawDocKey = encryptedDocKey; // Legacy public + } + + // If we have the raw key, return it immediately for the anonymous user + if (rawDocKey) { + return json({ + doc_id: decodedDocId, + snapshot, + envelopes: [], // No envelopes needed for public Key + rawKey: rawDocKey, // Send RAW key to anonymous user + title: note?.title ?? "Untitled", + ownerId: note?.ownerId, + accessLevel, + }); + } + } + + // Check if we have the key escrowed in the documents table + if (doc?.serverEncryptedKey) { + console.log( + "[JOIN] Found serverEncryptedKey. Attempting to broker key exchange...", + ); + try { + rawDocKey = await decryptKeyForDevice( + doc.serverEncryptedKey, + serverIdentity.encryptionPrivateKey, + ); + console.log( + "[JOIN] Successfully decrypted Note Key using Server Identity.", + ); + } catch (e) { + console.error("[JOIN] Server failed to decrypt escrowed key:", e); + } + } + + // Fallback: If no server key, check if existing doc key is already raw (public/legacy) + if (!rawDocKey) { + if (encryptedDocKey.length <= 44) { + console.log( + `[JOIN] Key appears to be raw (Length: ${encryptedDocKey.length})`, + ); + rawDocKey = encryptedDocKey; + } else { + // The key is encrypted (E2EE) and we don't have a broker copy. + // The server CANNOT decrypt the note key to re-encrypt it for the joining user. + console.warn( + `[JOIN] Request for E2EE note ${note?.id}. Server cannot fulfill automatically (No Escrow).`, + ); + error( + 424, + "E2EE_KEY_UNAVAILABLE: Server cannot decrypt note key. The owner must be online to approve or the note must be shared via client-side flow.", + ); + } + } + + // Special Handling for Password Protected Notes + if (accessLevel === "password_protected") { + console.log("[JOIN] Password Protected Note."); + if (doc?.passwordEncryptedKey) { + return json({ + doc_id: decodedDocId, + snapshot, + envelopes: [], + passwordEncryptedKey: doc.passwordEncryptedKey, + title: note?.title ?? "Untitled", + ownerId: note?.ownerId, + accessLevel, + }); + } else { + // If password key is missing, it's an error state for this mode + error( + 424, + "PASSWORD_KEY_UNAVAILABLE: Note is password protected but no password key was found.", + ); + } + } + + // Debug Identity Fetching + for (const handle of joiningUsers) { + await fetchUserIdentity(handle, requesting_server); + } + + const envelopes = await generateKeyEnvelopesForUsers( + rawDocKey, // Now passing the RAW key + joiningUsers, + requesting_server, + ); + + // Ensure documents entry exists (required for members FK constraint) + // Use the actual note ID (which may be a portable ID) + const noteId = note?.id ?? decodedDocId; + const docEntry = await db.query.documents.findFirst({ + where: eq(documents.id, noteId), + }); + + if (!docEntry) { + console.log(" Creating documents entry for:", noteId); + await db + .insert(documents) + .values({ + id: noteId, + hostServer: "local", + ownerId: note?.ownerId ?? "", + title: note?.title ?? "Untitled", + accessLevel: accessLevel, + createdAt: new Date(), + updatedAt: new Date(), + }) + .onConflictDoNothing(); + } + + // Also add the joining users as members + for (const envelope of envelopes) { + console.log(" Adding member:", envelope.user_id); + await db + .insert(members) + .values({ + docId: noteId, + userId: envelope.user_id, + deviceId: envelope.device_id, + role: "writer", + encryptedKeyEnvelope: envelope.encrypted_key, + createdAt: new Date(), + }) + .onConflictDoUpdate({ + target: [members.docId, members.userId, members.deviceId], + set: { + encryptedKeyEnvelope: envelope.encrypted_key, + role: "writer", + }, + }); + } + + return json({ + doc_id: decodedDocId, + snapshot, + envelopes, + title: note?.title ?? "Untitled", + ownerId: note?.ownerId, + accessLevel, + }); +} diff --git a/src/routes/federation/doc/[doc_id]/ops/+server.ts b/src/routes/federation/doc/[doc_id]/ops/+server.ts new file mode 100644 index 0000000..d6e7ae6 --- /dev/null +++ b/src/routes/federation/doc/[doc_id]/ops/+server.ts @@ -0,0 +1,103 @@ +import { json, error } from "@sveltejs/kit"; +import { verify } from "$lib/crypto"; +import { db } from "$lib/server/db"; +import { federatedOps } from "$lib/server/db/schema"; +import { eq, gt, asc, and } from "drizzle-orm"; +import { notePubSub } from "$lib/server/pubsub"; + +// Helper for verification (reuse from Join or export it? Duplicate for now to avoid logic split) +async function verifyServerRequest(request: Request, payload: any) { + const signature = request.headers.get("x-notes-signature"); + const timestamp = request.headers.get("x-notes-timestamp"); + const domain = request.headers.get("x-notes-domain"); + + if (!signature || !timestamp || !domain) error(401); + + const protocol = domain.includes("localhost") ? "http" : "https"; + const remoteKeyUrl = `${protocol}://${domain}/.well-known/notes-server`; + try { + const res = await fetch(remoteKeyUrl); + if (!res.ok) throw new Error(); + const data = await res.json(); + const msg = `${domain}:${timestamp}:${JSON.stringify(payload)}`; + const valid = await verify( + signature, + new TextEncoder().encode(msg), + data.publicKey, + ); + if (!valid) throw new Error(); + return data; + } catch { + error(401, "Verification failed"); + } +} + +// PULL Ops +export async function GET({ params, url }) { + const { doc_id } = params; + const since = url.searchParams.get("since"); + const sinceTs = since ? parseInt(since) : 0; + + const ops = await db.query.federatedOps.findMany({ + where: and( + eq(federatedOps.docId, doc_id), + gt(federatedOps.lamportTs, sinceTs), + ), + orderBy: [asc(federatedOps.lamportTs)], + }); + + return json({ + ops, + server_version: Date.now(), + }); +} + +// PUSH Ops +export async function POST({ params, request }) { + const { doc_id } = params; + console.log(`[FED] Received ops push for ${doc_id}`); + + const body = await request.json(); + const { ops } = body; + console.log(`[FED] Ops count: ${ops?.length}`); + + try { + await verifyServerRequest(request, body); + console.log(`[FED] Verification successful`); + } catch (e) { + console.error(`[FED] Verification failed:`, e); + throw e; + } + + if (!Array.isArray(ops)) error(400); + + // Validate that the document exists first? + // Ideally yes, but maybe we just accept ops for known docs. + + const normalizedOps = []; + + for (const op of ops) { + console.log(`[FED] Inserting op ${op.op_id}`); + const newOp = { + id: op.op_id, // ensure unique + docId: doc_id, + opId: op.op_id, + actorId: op.actor_id, + lamportTs: op.lamport_ts, + payload: op.encrypted_payload, // Note: client sends 'encrypted_payload' in JSON, but DB has 'payload' + signature: op.signature, + createdAt: new Date(), // Add timestamp + }; + + await db.insert(federatedOps).values(newOp).onConflictDoNothing(); + + normalizedOps.push(newOp); + } + + console.log(`[FED] Ops inserted successfully`); + + // Publish to PubSub for real-time subscribers + notePubSub.publish(doc_id, normalizedOps); + + return json({ success: true }); +} diff --git a/src/routes/federation/import/+page.server.ts b/src/routes/federation/import/+page.server.ts new file mode 100644 index 0000000..3b0d8a6 --- /dev/null +++ b/src/routes/federation/import/+page.server.ts @@ -0,0 +1,143 @@ +import { redirect, error, isHttpError } from "@sveltejs/kit"; +import { db } from "$lib/server/db"; +import { documents, members, notes } from "$lib/server/db/schema"; +import { eq } from "drizzle-orm"; +import { getServerIdentity, signServerRequest } from "$lib/server/identity"; +import type { User } from "$lib/schema.js"; + +export async function load({ url, locals }) { + if (!locals.user) { + redirect( + 302, + `/login?redirectTo=${encodeURIComponent(url.pathname + url.search)}`, + ); + } + + const doc_id = url.searchParams.get("doc_id"); + const host = url.searchParams.get("host"); + + if (!doc_id || !host) { + error(400, "Missing doc_id or host"); + } + + const identity = await getServerIdentity(); + const user: User = locals.user; + + // Check if we already have the doc + const existing = await db.query.documents.findFirst({ + where: eq(documents.id, doc_id), + }); + + if (existing) { + // Already imported, just redirect + redirect(302, `/notes/${doc_id}`); + } + + // Construct the federated handle for the joining user + const userHandle = `@${user.username}:${identity.domain}`; + + // Sign request + const payload = { + requesting_server: identity.domain, + users: [userHandle], // Full federated handle + }; + + const { signature, timestamp, domain } = await signServerRequest(payload); + + const protocol = host.includes("localhost") ? "http" : "https"; + const joinUrl = `${protocol}://${host}/federation/doc/${doc_id}/join`; + + let joinRes: { + snapshot: string | undefined; + envelopes: + | { + user_id: string | undefined; + device_id: string | undefined; + encrypted_key: string | undefined; + }[] + | undefined; + title: string | undefined; + accessLevel: string | undefined; + ownerId: string | undefined; + }; + try { + const res = await fetch(joinUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-notes-signature": signature, + "x-notes-timestamp": timestamp.toString(), + "x-notes-domain": domain, + }, + body: JSON.stringify(payload), + }); + + if (!res.ok) { + const text = await res.text(); + console.error("Join failed:", text); + error(res.status, `Failed to join document: ${text}`); + } + + joinRes = await res.json(); + } catch (e) { + console.error("Join error:", e); + if (isHttpError(e)) throw e; // Re-throw if it's already an error response + error(502, "Failed to contact host server"); + } + + // Find the envelope for our user + const myEnvelope = joinRes.envelopes?.find( + (env) => + env.user_id === userHandle || + env.user_id === `@${user.username}` || + env.user_id === user.username, + ); + + const encryptedKey = myEnvelope?.encrypted_key ?? ""; + + // Save Document Metadata + await db.insert(documents).values({ + id: doc_id, + hostServer: host, + ownerId: joinRes.ownerId ?? "unknown", + title: joinRes.title ?? "Untitled", + accessLevel: joinRes.accessLevel ?? "authenticated", + }); + + // Save Content (Snapshot) - use empty snapshot if none provided + await db + .insert(notes) + .values({ + id: doc_id, + ownerId: user.id, // Local user becomes local "owner" of this copy + title: joinRes.title ?? "Untitled", + encryptedKey, // The encrypted document key for this user + loroSnapshot: joinRes.snapshot ?? null, + accessLevel: joinRes.accessLevel ?? "authenticated", + }) + .onConflictDoUpdate({ + target: notes.id, + set: { + loroSnapshot: joinRes.snapshot ?? null, + encryptedKey, + updatedAt: new Date(), + }, + }); + + // Save all envelopes to members table + for (const env of joinRes.envelopes ?? []) { + await db + .insert(members) + .values({ + docId: doc_id, + userId: user.id, // Map to local user ID + deviceId: env.device_id ?? "primary", + role: "writer", + encryptedKeyEnvelope: env.encrypted_key, + createdAt: new Date(), + }) + .onConflictDoNothing(); + } + + redirect(302, `/notes/${doc_id}`); +} diff --git a/src/routes/logout/+server.ts b/src/routes/logout/+server.ts new file mode 100644 index 0000000..411b9fb --- /dev/null +++ b/src/routes/logout/+server.ts @@ -0,0 +1,13 @@ +import { redirect } from "@sveltejs/kit"; +import { invalidateSession, deleteSessionTokenCookie } from "$lib/server/auth"; + +export async function POST(event) { + if (!event.locals.session) { + return redirect(302, "/login"); + } + + await invalidateSession(event.locals.session.token); + deleteSessionTokenCookie(event.cookies); + + return redirect(302, "/login"); +} diff --git a/src/routes/notes/[id]/+layout.svelte b/src/routes/notes/[id]/+layout.svelte index c30ffe0..4b7cb06 100644 --- a/src/routes/notes/[id]/+layout.svelte +++ b/src/routes/notes/[id]/+layout.svelte @@ -1,16 +1,5 @@ -
- {#if data.user} - - {/if} - - {@render children()} -
+{@render children()} diff --git a/src/routes/notes/[id]/+page.server.ts b/src/routes/notes/[id]/+page.server.ts index 70f1ff7..c9bbea0 100644 --- a/src/routes/notes/[id]/+page.server.ts +++ b/src/routes/notes/[id]/+page.server.ts @@ -1,13 +1,18 @@ -import { getNotes } from "$lib/remote/notes.remote.ts"; -import { guardLogin } from "$lib/server/auth.ts"; -import { error } from "@sveltejs/kit"; +import { parseNoteId } from "$lib/noteId.ts"; +import { env } from "$env/dynamic/private"; -export const load = async ({ params }): Promise => { - guardLogin(); +export const load = ({ params }) => { + const { id } = params; + const currentDomain = env["SERVER_DOMAIN"] ?? "localhost:5173"; - const notesList = await getNotes(); - const note = notesList.find((n) => n.id === params.id); - if (note === undefined) { - error(404, "Note not found"); - } + // Parse note ID to check origin + const { origin, uuid } = parseNoteId(id); + + // Pass origin info to client for federation handling + return { + noteId: id, + noteUuid: uuid, + originServer: origin || currentDomain, + isLocal: !origin || origin === currentDomain, + }; }; diff --git a/src/routes/notes/[id]/+page.svelte b/src/routes/notes/[id]/+page.svelte index e1b8539..8b74b86 100644 --- a/src/routes/notes/[id]/+page.svelte +++ b/src/routes/notes/[id]/+page.svelte @@ -1,136 +1,434 @@ + +
- {#if !(note?.isFolder ?? true)} - { - // Hook in Loro - loroManager?.updateContent(newContent); - }} - /> - {:else if note?.isFolder} -
-
-

- - {note.title} -

-

Select a note inside to start editing.

+ {#if note} + {#if !note.isFolder} + + + + {#if loroManager && connectionStatus !== "connected"} +
+
+ + {#if connectionStatus === "disconnected"} + Disconnected. + + {:else if connectionStatus === "reconnecting"} + Reconnecting... + {:else} + Connecting... + {/if} + +
+
+ {/if} + {:else} +
+
+

+ + {note.title} +

+

Select a note inside to start editing.

+
-
+ {/if} {:else} -
-
-
- + +
+ {#if isJoining} +
+ +

Joining remote note...

-

No note selected

-

- Select a note from the sidebar or create a new one. -

-
+ {:else if joinError} +
+

Failed to Join Note

+

{joinError}

+ +
+ {:else if showPasswordPrompt} +
+

Password Protected Note

+

+ This note requires a password to access. +

+ + {#if passwordError} +
{passwordError}
+ {/if} + + e.key === "Enter" && handlePasswordSubmit()} + /> + +
+ + +
+
+ {:else if !data.user} +
+

+ You do not have access to this note +

+

+ This note is from {data.originServer}. It seems you + are not logged in or this note is private. If you are a user on + another server, enter your handle to open this note there. +

+ +
+ + e.key === "Enter" && + handleOpenInHomeserver(e.currentTarget.value)} + /> + +
+ +

+ Example: @alice:localhost.com +

+ +
OR
+ + + + Log in on this server + +
+ {:else} +
+
+ +
+

No note selected

+

+ Select a note from the sidebar or create a new one. +

+
+ {/if}
{/if}
@@ -141,9 +439,22 @@ class="pointer-events-none absolute right-4 bottom-4 z-50 max-w-sm rounded bg-black/80 p-4 font-mono text-xs text-white" >

Selected Note: {id}

+

Note Found: {!!note}

+

Note Title: {note?.title}

Loro Manager: {loroManager ? "Loaded" : "Null"}

Content Length: {editorContent.length}

Content Preview: {editorContent.slice(0, 50)}

~Word Count: {editorContent.split(/\s+/).length}

{/if} + + (redirectError = null)} + onCancel={() => (redirectError = null)} +/> diff --git a/src/routes/settings/account/+page.svelte b/src/routes/settings/account/+page.svelte new file mode 100644 index 0000000..bbf8a66 --- /dev/null +++ b/src/routes/settings/account/+page.svelte @@ -0,0 +1,143 @@ + + +
+

Account Settings

+ +
+
+

Change Password

+

+ This will re-encrypt your private key with your new password. You must + verify your old password to proceed (handled by server session check). + Wait - actually, since we are re-encrypting the RAW key in memory, we + don't STRICTLY need the old password if the vault is already unlocked. + But good practice implies asking for it. For this MVP, we will rely on + the fact that you are logged in + vault unlocked. +

+ + {#if statusMessage} +
{statusMessage}
+ {/if} + +
+ + + + +
+ + +
+ +
+ + +
+ +
+ +
+
+
+
+
diff --git a/svelte.config.js b/svelte.config.js index 16e6c94..5740ad3 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -18,9 +18,10 @@ const config = { typescript: { config(config) { - config["include"] = /** @type {string[]} */ (config["include"]).map( - (path) => path.replace("vite.config", "*.config"), - ); + config["include"] = /** @type {string[]} */ (config["include"]) + .map((path) => path.replace("vite.config", "*.config")) + .concat("../scripts/*.ts") + .concat("../src/routes/.well-known/**/*.ts"); }, }, },