diff --git a/hocuspocus-server/package-lock.json b/hocuspocus-server/package-lock.json index ab8e3ab..96b5e35 100644 --- a/hocuspocus-server/package-lock.json +++ b/hocuspocus-server/package-lock.json @@ -12,9 +12,9 @@ "@blocknote/core": "^0.25.1", "@blocknote/react": "^0.25.1", "@blocknote/server-util": "^0.25.0", - "@hocuspocus/common": "2.15.2", - "@hocuspocus/extension-sqlite": "^2.15.2", - "@hocuspocus/server": "^2.15.2", + "@hocuspocus/common": "3.0.6-rc.0", + "@hocuspocus/extension-sqlite": "3.0.6-rc.0", + "@hocuspocus/server": "3.0.6-rc.0", "@hono/node-server": "^1.0.8", "@hono/node-ws": "^1.0.8", "@tiptap/core": "2.11.5", @@ -27,6 +27,7 @@ "yjs": "^13.6.23" }, "devDependencies": { + "@types/node": "^20.11.18", "@typescript-eslint/eslint-plugin": "^5.5.0", "@typescript-eslint/parser": "^5.5.0", "eslint": "^8.10.0", @@ -2235,31 +2236,32 @@ "optional": true }, "node_modules/@hocuspocus/common": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/@hocuspocus/common/-/common-2.15.2.tgz", - "integrity": "sha512-wU1wxXNnQQMXyeL3mdSDYiQsm/r/QyJVjjQhF7sUBrLnjdsN7bA1cvfcSvJBr1ymrMSeYRmUL3UlQmEHEOaP7w==", + "version": "3.0.6-rc.0", + "resolved": "https://registry.npmjs.org/@hocuspocus/common/-/common-3.0.6-rc.0.tgz", + "integrity": "sha512-uDjU5l5O3ZukUBrK88M6m8fAT3ViLuEf3MgzFAYNYkv1PdeU9SmdhuM43pKLA3iFanqWD0MgMjkw6Pl1le5bZQ==", "dependencies": { "lib0": "^0.2.87" } }, "node_modules/@hocuspocus/extension-database": { - "version": "2.15.2", - "resolved": "https://registry.npmjs.org/@hocuspocus/extension-database/-/extension-database-2.15.2.tgz", - "integrity": "sha512-BkYDfKA99udx7AEkqWReBS61kvGMC9SqoPJs3v8xNgpaj2GGyMJQlUdQRMhPyZTn2osV+pqhk8Hn7xUJCW1RJg==", + "version": "3.0.6-rc.0", + "resolved": "https://registry.npmjs.org/@hocuspocus/extension-database/-/extension-database-3.0.6-rc.0.tgz", + "integrity": "sha512-5Z/n6QduaNxETrUT0DcSXCS9EGjUGAZF/vPVoqLEsaTb+GdjhuzgYjnRHa3AJvZDj5hFpAhEarRc5OBC4aNMHA==", "dependencies": { - "@hocuspocus/server": "^2.15.2" + "@hocuspocus/server": "^3.0.6-rc.0" }, "peerDependencies": { "yjs": "^13.6.8" } }, "node_modules/@hocuspocus/extension-sqlite": { - "version": "2.15.2", - "license": "MIT", + "version": "3.0.6-rc.0", + "resolved": "https://registry.npmjs.org/@hocuspocus/extension-sqlite/-/extension-sqlite-3.0.6-rc.0.tgz", + "integrity": "sha512-n5Az83H+o1XVO4u9xO1Qv5hOuJcR9THWfXd913LJe6GI1kh4VrRlCxk0+czpaVO6WDy9s+Dkf4ChoerNddFCDQ==", "dependencies": { - "@hocuspocus/extension-database": "^2.15.2", + "@hocuspocus/extension-database": "^3.0.6-rc.0", "kleur": "^4.1.4", - "sqlite3": "^5.0.11" + "sqlite3": "^5.1.7" } }, "node_modules/@hocuspocus/provider": { @@ -2279,11 +2281,22 @@ "yjs": "^13.6.8" } }, - "node_modules/@hocuspocus/server": { + "node_modules/@hocuspocus/provider/node_modules/@hocuspocus/common": { "version": "2.15.2", - "license": "MIT", + "resolved": "https://registry.npmjs.org/@hocuspocus/common/-/common-2.15.2.tgz", + "integrity": "sha512-wU1wxXNnQQMXyeL3mdSDYiQsm/r/QyJVjjQhF7sUBrLnjdsN7bA1cvfcSvJBr1ymrMSeYRmUL3UlQmEHEOaP7w==", + "optional": true, + "peer": true, "dependencies": { - "@hocuspocus/common": "^2.15.2", + "lib0": "^0.2.87" + } + }, + "node_modules/@hocuspocus/server": { + "version": "3.0.6-rc.0", + "resolved": "https://registry.npmjs.org/@hocuspocus/server/-/server-3.0.6-rc.0.tgz", + "integrity": "sha512-CnSRqvI0SIwBre+D2665hn/vFviYD2LvSo6SmZE3OvyVp9vdH9qrzmJaoJz3kMDgTbnLvcaVbPe2fUsNyToCqQ==", + "dependencies": { + "@hocuspocus/common": "^3.0.6-rc.0", "async-lock": "^1.3.1", "kleur": "^4.1.4", "lib0": "^0.2.47", @@ -3288,6 +3301,15 @@ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==" }, + "node_modules/@types/node": { + "version": "20.17.28", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.28.tgz", + "integrity": "sha512-DHlH/fNL6Mho38jTy7/JT7sn2wnXI+wULR6PV4gy4VHLVvnrV/d3pHAMQHhc4gjdLmK2ZiPoMxzp6B3yRajLSQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "dev": true, @@ -3938,7 +3960,8 @@ }, "node_modules/async-lock": { "version": "1.4.1", - "license": "MIT" + "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", + "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" }, "node_modules/asynckit": { "version": "0.4.0", @@ -11040,6 +11063,12 @@ "node": ">=18.17" } }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true + }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", "dev": true, @@ -11293,11 +11322,12 @@ }, "node_modules/uuid": { "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], - "license": "MIT", "bin": { "uuid": "dist/esm/bin/uuid" } diff --git a/hocuspocus-server/package.json b/hocuspocus-server/package.json index f334fc0..a63d154 100644 --- a/hocuspocus-server/package.json +++ b/hocuspocus-server/package.json @@ -21,9 +21,9 @@ "@blocknote/core": "^0.25.1", "@blocknote/react": "^0.25.1", "@blocknote/server-util": "^0.25.0", - "@hocuspocus/common": "2.15.2", - "@hocuspocus/extension-sqlite": "^2.15.2", - "@hocuspocus/server": "^2.15.2", + "@hocuspocus/common": "3.0.6-rc.0", + "@hocuspocus/extension-sqlite": "3.0.6-rc.0", + "@hocuspocus/server": "3.0.6-rc.0", "@hono/node-server": "^1.0.8", "@hono/node-ws": "^1.0.8", "@tiptap/core": "2.11.5", @@ -38,6 +38,7 @@ "devDependencies": { "@typescript-eslint/eslint-plugin": "^5.5.0", "@typescript-eslint/parser": "^5.5.0", + "@types/node": "^20.11.18", "eslint": "^8.10.0", "eslint-config-react-app": "^7.0.0", "eslint-plugin-import": "^2.31.0", diff --git a/hocuspocus-server/src/index.ts b/hocuspocus-server/src/index.ts index ee1c89f..fd64146 100644 --- a/hocuspocus-server/src/index.ts +++ b/hocuspocus-server/src/index.ts @@ -1,5 +1,5 @@ import { SQLite } from "@hocuspocus/extension-sqlite"; -import { Document, Server } from "@hocuspocus/server"; +import { type Document, Hocuspocus } from "@hocuspocus/server"; import { serve } from "@hono/node-server"; import { createNodeWebSocket } from "@hono/node-ws"; @@ -10,7 +10,8 @@ import { FAKE_authInfoFromToken } from "./auth.js"; import { threadsRouter } from "./threads.js"; import { RejectUnauthorized } from "./rejectUnauthorized.js"; // Setup Hocuspocus server -const hocuspocusServer = Server.configure({ + +const hocuspocus = new Hocuspocus({ async onAuthenticate(data) { const { token } = data; @@ -19,15 +20,19 @@ const hocuspocusServer = Server.configure({ if (authInfo === "unauthorized") { throw new Error("Not authorized!"); } - - data.connection.readOnly = authInfo.role === "COMMENT-ONLY"; + + data.connectionConfig.readOnly = authInfo.role === "COMMENT-ONLY"; }, extensions: [ new SQLite({ - database: "db.sqlite", + database: ":memory:", + }), + // TODO we can actually just do the auth check in here, and not need the server to inject the mark or anything + new RejectUnauthorized("threads", (payload) => { + // eslint-disable-next-line no-console + console.warn("rejecting update to document", payload.documentName); }), - new RejectUnauthorized("threads"), ], // TODO: for good security, you'd want to make sure that either: @@ -46,7 +51,7 @@ app.get( "/hocuspocus", upgradeWebSocket((c) => ({ onOpen(_evt, ws) { - hocuspocusServer.handleConnection(ws.raw, c.req.raw); + hocuspocus.handleConnection(ws.raw, c.req.raw as any); }, })) ); @@ -61,7 +66,7 @@ const documentMiddleware = createMiddleware<{ }; }>(async (c, next) => { const documentId = c.req.param("documentId"); - const document = hocuspocusServer.documents.get(documentId!); + const document = hocuspocus.documents.get(documentId!); if (!document) { return c.json({ error: "Document not found" }, 404); @@ -85,6 +90,12 @@ app.route( const server = serve({ fetch: app.fetch, port: 8787, +}, (info) => { + hocuspocus.hooks('onListen', { + instance: hocuspocus, + configuration: hocuspocus.configuration, + port: info.port + }) }); // Setup WebSocket support (needed for HocusPocus) diff --git a/hocuspocus-server/src/rejectUnauthorized.ts b/hocuspocus-server/src/rejectUnauthorized.ts index c4013e6..5684863 100644 --- a/hocuspocus-server/src/rejectUnauthorized.ts +++ b/hocuspocus-server/src/rejectUnauthorized.ts @@ -1,10 +1,4 @@ -import type { CloseEvent } from "@hocuspocus/common"; -import { - beforeHandleMessagePayload, - Extension, - IncomingMessage, - MessageType, -} from "@hocuspocus/server"; +import { type beforeSyncPayload, Extension } from "@hocuspocus/server"; import * as syncProtocol from "y-protocols/sync"; import * as Y from "yjs"; @@ -18,55 +12,10 @@ import * as Y from "yjs"; * - if the update is accepted, we do nothing */ export class RejectUnauthorized implements Extension { - constructor(private readonly threadsMapKey: string) {} - /** - * Extract the yjsUpdate from the incoming message - * @param message - * @returns - */ - private getYUpdate(message: Uint8Array) { - /** - * The messages we are interested in are of the following format: - * [docIdLength: number, ...docIdString: string, hocuspocusMessageType: number, ySyncMessageType: number, ...yjsUpdate: Uint8Array] - * - * We check that the hocuspocusMessageType is Sync and that the ySyncMessageType is messageYjsUpdate. - */ - const incomingMessage = new IncomingMessage(message); - // Read the docID string, but don't use it - incomingMessage.readVarString(); - - // Read the hocuspocusMessageType - const hocuspocusMessageType = incomingMessage.readVarUint(); - // If the hocuspocusMessageType is not Sync, we don't handle the message, since it is not an update - if ( - !( - hocuspocusMessageType === MessageType.Sync || - hocuspocusMessageType === MessageType.SyncReply - ) - ) { - return; - } - - // Read the ySyncMessageType - const ySyncMessageType = incomingMessage.readVarUint(); - - // If the ySyncMessageType is not a messageYjsUpdate or a messageYjsSyncStep2, we don't handle the message, since it is not an update - if ( - !( - ySyncMessageType === syncProtocol.messageYjsUpdate || - ySyncMessageType === syncProtocol.messageYjsSyncStep2 - ) - ) { - // not an update we want to handle - return; - } - - // Read the yjsUpdate - const yUpdate = incomingMessage.readVarUint8Array(); - - return yUpdate; - } - + constructor( + private readonly threadsMapKey: string, + private readonly onReject?: (payload: beforeSyncPayload) => void + ) {} /** * This function protects against changes to the restricted type. * It does this by: @@ -112,29 +61,31 @@ export class RejectUnauthorized implements Extension { return didNeedToUndo; } - async beforeHandleMessage({ - update, - document: ydoc, - }: beforeHandleMessagePayload) { - const yUpdate = this.getYUpdate(update); - - if (!yUpdate) { + /** + * Before the document is synchronized, we check if the update modifies the restricted type. + * If it does, we reject the update by undoing it, and calling the onReject callback. + */ + async beforeSync(data: beforeSyncPayload) { + // If the ySyncMessageType is not a messageYjsUpdate or a messageYjsSyncStep2, we don't handle the message, since it is not an update + if ( + !( + data.type === syncProtocol.messageYjsUpdate || + data.type === syncProtocol.messageYjsSyncStep2 + ) + ) { + // not an update we want to handle return; } - const protectedType = ydoc.getMap(this.threadsMapKey); + const protectedType = data.document.getMap(this.threadsMapKey); const didRollback = this.applyUpdateAndRollbackIfNeeded( - yUpdate, - ydoc, + data.payload, + data.document, protectedType ); if (didRollback) { - // TODO, we can close their connection or just let them continue, since we've already undone their changes (and our changes are newer than theirs) - const error = { - reason: `Modification of a restricted type: ${this.threadsMapKey} was rejected`, - } satisfies Partial; - throw error; + this.onReject?.(data); } } } diff --git a/next-app/package-lock.json b/next-app/package-lock.json index 187e8fa..1222d32 100644 --- a/next-app/package-lock.json +++ b/next-app/package-lock.json @@ -8,9 +8,9 @@ "name": "next-app", "version": "0.1.0", "dependencies": { - "@blocknote/core": "^0.25.1", - "@blocknote/mantine": "^0.25.1", - "@blocknote/react": "^0.25.1", + "@blocknote/core": "^0.26.0", + "@blocknote/mantine": "^0.26.0", + "@blocknote/react": "^0.26.0", "@hocuspocus/provider": "^2.15.2", "next": "15.1.7", "react": "^19.0.0", @@ -52,9 +52,9 @@ } }, "node_modules/@blocknote/core": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@blocknote/core/-/core-0.25.1.tgz", - "integrity": "sha512-mlpRY5C2zBOaVJde53ArnKqyHTVzYcDhnQtjiZhLHmtYAJip5/Kc5VUd12rIKaGxMSjAhMixHavEzbuFOpQv2Q==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@blocknote/core/-/core-0.26.0.tgz", + "integrity": "sha512-IcG53FYFEn2IwwCZOMyczrRe4WH7HqPB4J0DW9LeUefTg78uVj/iuo0lXKJarHpIrED4yqvkYUzL76h0wH3fxQ==", "dependencies": { "@emoji-mart/data": "^1.2.1", "@tiptap/core": "^2.11.5", @@ -110,12 +110,12 @@ } }, "node_modules/@blocknote/mantine": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@blocknote/mantine/-/mantine-0.25.1.tgz", - "integrity": "sha512-oILa+bP7Xh10M67x/pw07rkBsTo8n80lhNpvyCS81q33NSiKEZ33z3603Jcagf/LBUQU+m8rKSfWBFcAlePmCw==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@blocknote/mantine/-/mantine-0.26.0.tgz", + "integrity": "sha512-LaIlpR/Ce0zCxojURLS9ok78GG921V9zv0e/NQDGQM+rd2ZkMY7zbVMFjJSVKXs6voHQhBy5ZIiH/7ig3lbLCw==", "dependencies": { - "@blocknote/core": "^0.25.1", - "@blocknote/react": "^0.25.1", + "@blocknote/core": "^0.26.0", + "@blocknote/react": "^0.26.0", "@mantine/core": "^7.10.1", "@mantine/hooks": "^7.10.1", "@mantine/utils": "^6.0.21", @@ -127,11 +127,11 @@ } }, "node_modules/@blocknote/react": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/@blocknote/react/-/react-0.25.1.tgz", - "integrity": "sha512-sciHltGLMvgpHPbKFllOGJmz+iCKsfJmTPrDcoWavuv5PgQ366aUNnbLOAPP6GmlkGLeS00y9Tk/C+5It8ybtA==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/@blocknote/react/-/react-0.26.0.tgz", + "integrity": "sha512-D6/ZexM53jStW9bFRF7469teYXJUN6HFS6apgO8TujflI/PITXtuVL39WxNkZUwN0I0dbum+1qNxV9GWXQXakA==", "dependencies": { - "@blocknote/core": "^0.25.1", + "@blocknote/core": "^0.26.0", "@emoji-mart/data": "^1.2.1", "@floating-ui/react": "^0.26.4", "@tiptap/core": "^2.7.1", diff --git a/next-app/package.json b/next-app/package.json index ee6ab34..0a68098 100644 --- a/next-app/package.json +++ b/next-app/package.json @@ -10,9 +10,9 @@ }, "dependencies": { "@hocuspocus/provider": "^2.15.2", - "@blocknote/core": "^0.25.1", - "@blocknote/mantine": "^0.25.1", - "@blocknote/react": "^0.25.1", + "@blocknote/core": "^0.26.0", + "@blocknote/mantine": "^0.26.0", + "@blocknote/react": "^0.26.0", "next": "15.1.7", "react": "^19.0.0", "react-dom": "^19.0.0"