@mp-lb/mdkit 0.3.2 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/dist/collaboration/useMdKitCollaboration.d.ts +5 -0
- package/dist/collaboration/useMdKitCollaboration.d.ts.map +1 -0
- package/dist/collaboration/useMdKitCollaboration.js +4 -0
- package/dist/core/checkpointPolicy.d.ts +10 -0
- package/dist/core/checkpointPolicy.d.ts.map +1 -0
- package/dist/core/checkpointPolicy.js +9 -0
- package/dist/core/documentEngine.d.ts +1 -0
- package/dist/core/documentEngine.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/document/MdKitConflictPanel.d.ts +5 -0
- package/dist/document/MdKitConflictPanel.d.ts.map +1 -0
- package/dist/document/MdKitConflictPanel.js +4 -0
- package/dist/document/MdKitDocumentToolbar.d.ts +6 -0
- package/dist/document/MdKitDocumentToolbar.d.ts.map +1 -0
- package/dist/document/MdKitDocumentToolbar.js +5 -0
- package/dist/document/documentTypes.d.ts +6 -0
- package/dist/document/documentTypes.d.ts.map +1 -0
- package/dist/document/useMdKitDocument.d.ts +5 -0
- package/dist/document/useMdKitDocument.d.ts.map +1 -0
- package/dist/document/useMdKitDocument.js +4 -0
- package/dist/fastify.d.ts +1 -0
- package/dist/fastify.d.ts.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -0
- package/dist/markdown/MarkdownBubbleMenu.d.ts +1 -0
- package/dist/markdown/MarkdownBubbleMenu.d.ts.map +1 -0
- package/dist/markdown/MarkdownPasteExtension.d.ts +1 -0
- package/dist/markdown/MarkdownPasteExtension.d.ts.map +1 -0
- package/dist/markdown/MarkdownSearchExtension.d.ts +1 -0
- package/dist/markdown/MarkdownSearchExtension.d.ts.map +1 -0
- package/dist/markdown/MarkdownSearchPanel.d.ts +1 -0
- package/dist/markdown/MarkdownSearchPanel.d.ts.map +1 -0
- package/dist/markdown/MdKitEditor.d.ts +11 -0
- package/dist/markdown/MdKitEditor.d.ts.map +1 -0
- package/dist/markdown/MdKitEditor.js +10 -2
- package/dist/markdown/MdKitView.d.ts +9 -1
- package/dist/markdown/MdKitView.d.ts.map +1 -0
- package/dist/markdown/MdKitView.js +7 -2
- package/dist/markdown/TiptapMarkdownSurface.d.ts +1 -0
- package/dist/markdown/TiptapMarkdownSurface.d.ts.map +1 -0
- package/dist/markdown/TiptapMarkdownSurface.js +3 -22
- package/dist/markdown/createMdKitTiptapExtensions.d.ts +1 -0
- package/dist/markdown/createMdKitTiptapExtensions.d.ts.map +1 -0
- package/dist/markdown/editorDebug.d.ts +1 -0
- package/dist/markdown/editorDebug.d.ts.map +1 -0
- package/dist/markdown/markdownFenceRanges.d.ts +1 -0
- package/dist/markdown/markdownFenceRanges.d.ts.map +1 -0
- package/dist/markdown/normalizeMarkdownSerialization.d.ts +1 -0
- package/dist/markdown/normalizeMarkdownSerialization.d.ts.map +1 -0
- package/dist/markdown/prepareMarkdownForEditorHydration.d.ts +1 -0
- package/dist/markdown/prepareMarkdownForEditorHydration.d.ts.map +1 -0
- package/dist/markdown/preserveMarkdownWhitespace.d.ts +1 -0
- package/dist/markdown/preserveMarkdownWhitespace.d.ts.map +1 -0
- package/dist/markdown/yamlFrontMatter.d.ts +1 -0
- package/dist/markdown/yamlFrontMatter.d.ts.map +1 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/theme/MdKitThemeEditor.d.ts +5 -0
- package/dist/theme/MdKitThemeEditor.d.ts.map +1 -0
- package/dist/theme/MdKitThemeEditor.js +4 -0
- package/dist/theme/editorTheme.d.ts +1 -0
- package/dist/theme/editorTheme.d.ts.map +1 -0
- package/dist/theme/editorTheme.js +8 -8
- package/dist/transport/backend.d.ts +13 -0
- package/dist/transport/backend.d.ts.map +1 -0
- package/dist/transport/backend.js +6 -0
- package/dist/transport/fastify.d.ts +5 -0
- package/dist/transport/fastify.d.ts.map +1 -0
- package/dist/transport/fastify.js +4 -0
- package/dist/transport/http.d.ts +1 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/index.d.ts +1 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/rest.d.ts +6 -0
- package/dist/transport/rest.d.ts.map +1 -0
- package/dist/transport/rest.js +5 -0
- package/dist/transport/store.d.ts +1 -0
- package/dist/transport/store.d.ts.map +1 -0
- package/dist/transport/trpcClient.d.ts +8 -0
- package/dist/transport/trpcClient.d.ts.map +1 -0
- package/dist/transport/trpcClient.js +7 -0
- package/dist/transport/trpcServer.d.ts +6 -0
- package/dist/transport/trpcServer.d.ts.map +1 -0
- package/dist/transport/trpcServer.js +5 -0
- package/dist/trpc/client.d.ts +1 -0
- package/dist/trpc/client.d.ts.map +1 -0
- package/dist/trpc/server.d.ts +1 -0
- package/dist/trpc/server.d.ts.map +1 -0
- package/dist/trpc.d.ts +1 -0
- package/dist/trpc.d.ts.map +1 -0
- package/dist/ui/joinClassNames.d.ts +1 -0
- package/dist/ui/joinClassNames.d.ts.map +1 -0
- package/dist/versioning/VersionHistoryPanel.d.ts +5 -0
- package/dist/versioning/VersionHistoryPanel.d.ts.map +1 -0
- package/dist/versioning/VersionHistoryPanel.js +4 -0
- package/dist/versioning/useMdKitDocumentVersions.d.ts +5 -0
- package/dist/versioning/useMdKitDocumentVersions.d.ts.map +1 -0
- package/dist/versioning/useMdKitDocumentVersions.js +4 -0
- package/dist/yjs/MdKitMarkdownYjs.d.ts +1 -0
- package/dist/yjs/MdKitMarkdownYjs.d.ts.map +1 -0
- package/dist/yjs/index.d.ts +1 -0
- package/dist/yjs/index.d.ts.map +1 -0
- package/package.json +10 -12
- package/src/collaboration/useMdKitCollaboration.ts +528 -0
- package/src/core/checkpointPolicy.ts +107 -0
- package/src/core/documentEngine.ts +175 -0
- package/src/core/index.ts +33 -0
- package/src/document/MdKitConflictPanel.tsx +129 -0
- package/src/document/MdKitDocumentToolbar.tsx +141 -0
- package/src/document/documentTypes.ts +89 -0
- package/src/document/useMdKitDocument.ts +543 -0
- package/src/fastify.ts +6 -0
- package/src/index.ts +89 -0
- package/src/markdown/MarkdownBubbleMenu.tsx +271 -0
- package/src/markdown/MarkdownPasteExtension.ts +81 -0
- package/src/markdown/MarkdownSearchExtension.ts +77 -0
- package/src/markdown/MarkdownSearchPanel.tsx +98 -0
- package/src/markdown/MdKitEditor.tsx +75 -0
- package/src/markdown/MdKitView.tsx +80 -0
- package/src/markdown/TiptapMarkdownSurface.tsx +923 -0
- package/src/markdown/createMdKitTiptapExtensions.ts +42 -0
- package/src/markdown/editorDebug.ts +5 -0
- package/src/markdown/markdownFenceRanges.ts +68 -0
- package/src/markdown/normalizeMarkdownSerialization.ts +55 -0
- package/src/markdown/prepareMarkdownForEditorHydration.ts +23 -0
- package/src/markdown/preserveMarkdownWhitespace.ts +143 -0
- package/src/markdown/yamlFrontMatter.ts +135 -0
- package/src/server.ts +6 -0
- package/src/styles.css +125 -53
- package/src/theme/MdKitThemeEditor.tsx +134 -0
- package/src/theme/editorTheme.ts +72 -0
- package/src/transport/backend.ts +220 -0
- package/src/transport/fastify.ts +57 -0
- package/src/transport/http.ts +126 -0
- package/src/transport/index.ts +12 -0
- package/src/transport/rest.ts +80 -0
- package/src/transport/store.ts +45 -0
- package/src/transport/trpcClient.ts +90 -0
- package/src/transport/trpcServer.ts +66 -0
- package/src/trpc/client.ts +11 -0
- package/src/trpc/server.ts +12 -0
- package/src/trpc.ts +11 -0
- package/src/ui/joinClassNames.ts +3 -0
- package/src/versioning/VersionHistoryPanel.tsx +146 -0
- package/src/versioning/useMdKitDocumentVersions.ts +146 -0
- package/src/yjs/MdKitMarkdownYjs.ts +111 -0
- package/src/yjs/index.ts +8 -0
- package/docs/.vitepress/config.ts +0 -47
- package/docs/api.md +0 -512
- package/docs/architecture.md +0 -96
- package/docs/collaboration-persistence.md +0 -147
- package/docs/index.md +0 -341
- package/docs/permissions.md +0 -139
- package/docs/plain-text.md +0 -131
- package/docs/rest.md +0 -98
- package/docs/shadcn.md +0 -125
- package/docs/styling.md +0 -373
- package/docs/use-cases.md +0 -148
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
# Collaboration Persistence
|
|
2
|
-
|
|
3
|
-
`useMdKitCollaboration` connects `MdKitEditor` to Hocuspocus/Yjs. Mdkit handles
|
|
4
|
-
the editor integration and markdown/Yjs conversion helpers, but your application
|
|
5
|
-
owns where collaboration state is stored.
|
|
6
|
-
|
|
7
|
-
## What Needs To Be Stored
|
|
8
|
-
|
|
9
|
-
Hocuspocus collaboration state is Yjs binary state for one collaborative
|
|
10
|
-
document. Store it as bytes in your database or object storage. Good fits
|
|
11
|
-
include Postgres `bytea`, MongoDB `Binary`, S3/blob storage, or another durable
|
|
12
|
-
store your backend already uses.
|
|
13
|
-
|
|
14
|
-
You can keep this state in memory for a single-process app. That is simple and
|
|
15
|
-
can be acceptable for small deployments, but it has important limits:
|
|
16
|
-
|
|
17
|
-
- a server restart loses open collaboration state unless it was flushed
|
|
18
|
-
elsewhere
|
|
19
|
-
- multiple backend instances do not share memory
|
|
20
|
-
- collaborators can split across different instances unless your deployment
|
|
21
|
-
adds sticky routing, shared persistence, or Hocuspocus infrastructure for
|
|
22
|
-
multi-instance collaboration
|
|
23
|
-
|
|
24
|
-
## Document Names Are Stable IDs
|
|
25
|
-
|
|
26
|
-
Hocuspocus calls the collaboration key `documentName`. Treat that value as a
|
|
27
|
-
stable unique document identifier, not as a mutable display name. It can be a
|
|
28
|
-
document UUID, file id, or deterministic tenant/document key such as
|
|
29
|
-
`tenantId:documentId`.
|
|
30
|
-
|
|
31
|
-
On the client, mdkit passes `documentId` to Hocuspocus as the provider `name` by
|
|
32
|
-
default. If your collaboration key needs tenancy or another namespace, derive it
|
|
33
|
-
with `resolveRoomName`:
|
|
34
|
-
|
|
35
|
-
```tsx
|
|
36
|
-
const collaboration = useMdKitCollaboration({
|
|
37
|
-
collaborator: { id: user.id, name: user.name },
|
|
38
|
-
documentId,
|
|
39
|
-
endpoint: `${apiUrl.replace(/^http/, "ws")}/collaboration`,
|
|
40
|
-
getToken,
|
|
41
|
-
resolveRoomName: (documentId) => `${tenantId}:${documentId}`,
|
|
42
|
-
});
|
|
43
|
-
```
|
|
44
|
-
|
|
45
|
-
The same resolved value is what Hocuspocus passes to its persistence callbacks
|
|
46
|
-
as `documentName`.
|
|
47
|
-
|
|
48
|
-
## Database Extension
|
|
49
|
-
|
|
50
|
-
The usual integration is Hocuspocus' generic `Database` extension. Put your DB
|
|
51
|
-
reads and writes inside `fetch` and `store`.
|
|
52
|
-
|
|
53
|
-
```ts
|
|
54
|
-
import { Database } from "@hocuspocus/extension-database";
|
|
55
|
-
import { Server } from "@hocuspocus/server";
|
|
56
|
-
import { CheckpointPolicy } from "@mp-lb/mdkit/core";
|
|
57
|
-
import {
|
|
58
|
-
createMdKitBackend,
|
|
59
|
-
type MdKitBackendStore,
|
|
60
|
-
} from "@mp-lb/mdkit/server";
|
|
61
|
-
|
|
62
|
-
const mdkit = createMdKitBackend({
|
|
63
|
-
store: createYourDocumentStore(),
|
|
64
|
-
checkpointPolicy: CheckpointPolicy.smart(),
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
const collaboration = Server.configure({
|
|
68
|
-
extensions: [
|
|
69
|
-
new Database({
|
|
70
|
-
fetch: async ({ documentName }) => {
|
|
71
|
-
// Hocuspocus calls this documentName, but it should be a stable id.
|
|
72
|
-
return mdkit.readCollaborationState?.(documentName) ?? null;
|
|
73
|
-
},
|
|
74
|
-
|
|
75
|
-
store: async ({ documentName, state }) => {
|
|
76
|
-
await mdkit.writeCollaborationState?.(documentName, state);
|
|
77
|
-
},
|
|
78
|
-
}),
|
|
79
|
-
],
|
|
80
|
-
});
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
Your document store can back those mdkit methods with any database:
|
|
84
|
-
|
|
85
|
-
```ts
|
|
86
|
-
const createYourDocumentStore = (): MdKitBackendStore => ({
|
|
87
|
-
// Current markdown snapshot.
|
|
88
|
-
readDocument: (documentId) => db.documents.readCurrent(documentId),
|
|
89
|
-
writeDocument: (input) => db.documents.writeCurrent(input),
|
|
90
|
-
|
|
91
|
-
// Optional checkpoint history.
|
|
92
|
-
createCheckpoint: (input) => db.checkpoints.create(input),
|
|
93
|
-
getLatestCheckpoint: (documentId) => db.checkpoints.latest(documentId),
|
|
94
|
-
listDocumentVersions: (documentId) => db.checkpoints.list(documentId),
|
|
95
|
-
readDocumentVersion: (input) => db.checkpoints.read(input),
|
|
96
|
-
|
|
97
|
-
// Durable Yjs collaboration state.
|
|
98
|
-
readCollaborationState: async (documentName) => {
|
|
99
|
-
const row = await db.collaborationState.findByDocumentName(documentName);
|
|
100
|
-
return row?.yjsState ?? null;
|
|
101
|
-
},
|
|
102
|
-
|
|
103
|
-
writeCollaborationState: async (documentName, state) => {
|
|
104
|
-
await db.collaborationState.upsert({
|
|
105
|
-
documentName,
|
|
106
|
-
yjsState: state,
|
|
107
|
-
updatedAt: new Date().toISOString(),
|
|
108
|
-
});
|
|
109
|
-
},
|
|
110
|
-
});
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
Store the `state` bytes exactly as Hocuspocus gives them to you, and return the
|
|
114
|
-
same bytes from `fetch`. Do not convert the Yjs state to JSON and rebuild it on
|
|
115
|
-
every connection; that creates new Yjs identities and can duplicate content when
|
|
116
|
-
clients reconnect.
|
|
117
|
-
|
|
118
|
-
## Seeding From Markdown
|
|
119
|
-
|
|
120
|
-
The simplest flow is:
|
|
121
|
-
|
|
122
|
-
1. Current markdown lives in your normal document table.
|
|
123
|
-
2. Hocuspocus asks for Yjs state in `fetch`.
|
|
124
|
-
3. If persisted Yjs state exists, return it.
|
|
125
|
-
4. If no persisted Yjs state exists, seed the collaboration room from the
|
|
126
|
-
current markdown snapshot.
|
|
127
|
-
5. After that first seed, persist and reuse the Yjs state.
|
|
128
|
-
|
|
129
|
-
Mdkit exposes Yjs helpers for that seed/reset work:
|
|
130
|
-
|
|
131
|
-
```ts
|
|
132
|
-
import { yjs } from "@mp-lb/mdkit";
|
|
133
|
-
|
|
134
|
-
const state = existingState ?? yjs.markdownToMdKitYjs(currentMarkdown);
|
|
135
|
-
```
|
|
136
|
-
|
|
137
|
-
If your app supports restore, import, or external full-document replacement,
|
|
138
|
-
replace or reset the persisted collaboration state deliberately so the next
|
|
139
|
-
collaboration session starts from the restored canonical markdown.
|
|
140
|
-
|
|
141
|
-
## Hocuspocus Docs
|
|
142
|
-
|
|
143
|
-
For the Hocuspocus side of the setup, see:
|
|
144
|
-
|
|
145
|
-
- [Hocuspocus Database extension](https://tiptap.dev/docs/hocuspocus/server/extensions/database)
|
|
146
|
-
- [Hocuspocus persistence guide](https://tiptap.dev/docs/hocuspocus/guides/persistence)
|
|
147
|
-
- [Hocuspocus Redis extension](https://tiptap.dev/docs/hocuspocus/server/extensions/redis)
|
package/docs/index.md
DELETED
|
@@ -1,341 +0,0 @@
|
|
|
1
|
-
# Quick Start
|
|
2
|
-
|
|
3
|
-
Markdown Editor Kit is a React package for teams that need more than a bare
|
|
4
|
-
markdown editor widget. It starts with a controlled markdown editor that behaves
|
|
5
|
-
like a textarea, then adds autosave, checkpoint history, conflict handling, and
|
|
6
|
-
collaboration through adapters.
|
|
7
|
-
|
|
8
|
-
Use the pieces you need. `MdKitEditor` works without any backend, storage,
|
|
9
|
-
checkpoint history, or collaboration. Persistence, checkpoint history, and
|
|
10
|
-
collaboration are optional layers that can be added independently.
|
|
11
|
-
|
|
12
|
-
```bash
|
|
13
|
-
pnpm add @mp-lb/mdkit
|
|
14
|
-
```
|
|
15
|
-
|
|
16
|
-
Import the stylesheet once if you want reset-resistant markdown defaults and
|
|
17
|
-
generic fallback styling for the base panels:
|
|
18
|
-
|
|
19
|
-
```ts
|
|
20
|
-
import "@mp-lb/mdkit/styles.css";
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
See [Styling](./styling.md) for reset handling, dark mode, fonts, sizing, and
|
|
24
|
-
custom panel styles.
|
|
25
|
-
|
|
26
|
-
## Basic Editor
|
|
27
|
-
|
|
28
|
-
`MdKitEditor` is the textarea-like entry point. It has no persistence, no
|
|
29
|
-
checkpoint history, and no collaboration. You own the `value` and `onChange`
|
|
30
|
-
state.
|
|
31
|
-
|
|
32
|
-
```tsx
|
|
33
|
-
import { useState } from "react";
|
|
34
|
-
import { MdKitEditor } from "@mp-lb/mdkit";
|
|
35
|
-
import "@mp-lb/mdkit/styles.css";
|
|
36
|
-
|
|
37
|
-
export function MarkdownEditorExample() {
|
|
38
|
-
const [markdown, setMarkdown] = useState("# Hello markdown");
|
|
39
|
-
return <MdKitEditor value={markdown} onChange={setMarkdown} />;
|
|
40
|
-
}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Use this when you want a local editor, a form field, or a debug surface.
|
|
44
|
-
|
|
45
|
-
## Read-only View
|
|
46
|
-
|
|
47
|
-
`MdKitView` renders markdown with the same package stylesheet, CSS variables,
|
|
48
|
-
and `fillHeight` sizing contract as `MdKitEditor`, but it does not mount Tiptap
|
|
49
|
-
or ProseMirror.
|
|
50
|
-
|
|
51
|
-
```tsx
|
|
52
|
-
import { MdKitView } from "@mp-lb/mdkit";
|
|
53
|
-
import "@mp-lb/mdkit/styles.css";
|
|
54
|
-
|
|
55
|
-
export function MarkdownPreview({ markdown }: { markdown: string }) {
|
|
56
|
-
return <MdKitView fillHeight value={markdown} />;
|
|
57
|
-
}
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Use this for document previews, restored-checkpoint views, or any readonly
|
|
61
|
-
markdown surface that should visually match the editor.
|
|
62
|
-
|
|
63
|
-
## Connected Editor
|
|
64
|
-
|
|
65
|
-
The connected workflow combines:
|
|
66
|
-
|
|
67
|
-
- `useMdKitDocument` for loading, autosave, dirty state, and conflict detection
|
|
68
|
-
- `useMdKitDocumentVersions` for checkpoint browsing and restore
|
|
69
|
-
- `useMdKitCollaboration` for Hocuspocus/Yjs collaboration
|
|
70
|
-
- `MdKitDocumentToolbar`, `VersionHistoryPanel`, and `MdKitConflictPanel` for
|
|
71
|
-
a complete base-panel UI
|
|
72
|
-
|
|
73
|
-
The TypeScript-first path is tRPC. REST is also supported for high-compatibility
|
|
74
|
-
backends and non-TypeScript stacks.
|
|
75
|
-
|
|
76
|
-
### Frontend With tRPC
|
|
77
|
-
|
|
78
|
-
```tsx
|
|
79
|
-
import { useMemo, useState } from "react";
|
|
80
|
-
import {
|
|
81
|
-
MdKitConflictPanel,
|
|
82
|
-
MdKitDocumentToolbar,
|
|
83
|
-
MdKitEditor,
|
|
84
|
-
VersionHistoryPanel,
|
|
85
|
-
useMdKitCollaboration,
|
|
86
|
-
useMdKitDocument,
|
|
87
|
-
useMdKitDocumentVersions,
|
|
88
|
-
type MdKitDocumentVersionDetail,
|
|
89
|
-
} from "@mp-lb/mdkit";
|
|
90
|
-
import { createMdKitTrpcAdapter } from "@mp-lb/mdkit/trpc/client";
|
|
91
|
-
import { createTRPCProxyClient, httpBatchLink } from "@trpc/client";
|
|
92
|
-
import type { AppRouter } from "./server";
|
|
93
|
-
|
|
94
|
-
const documentId = "docs/example.md";
|
|
95
|
-
|
|
96
|
-
export function ConnectedMarkdownEditor({
|
|
97
|
-
apiUrl,
|
|
98
|
-
}: {
|
|
99
|
-
apiUrl: string;
|
|
100
|
-
}) {
|
|
101
|
-
const [versionHistoryOpen, setVersionHistoryOpen] = useState(false);
|
|
102
|
-
const [conflictOpen, setConflictOpen] = useState(false);
|
|
103
|
-
|
|
104
|
-
const trpc = useMemo(
|
|
105
|
-
() =>
|
|
106
|
-
createTRPCProxyClient<AppRouter>({
|
|
107
|
-
links: [httpBatchLink({ url: `${apiUrl}/trpc` })],
|
|
108
|
-
}),
|
|
109
|
-
[apiUrl],
|
|
110
|
-
);
|
|
111
|
-
|
|
112
|
-
const adapter = useMemo(
|
|
113
|
-
() => createMdKitTrpcAdapter({ client: trpc.mdkit }),
|
|
114
|
-
[trpc],
|
|
115
|
-
);
|
|
116
|
-
const document = useMdKitDocument({
|
|
117
|
-
adapter,
|
|
118
|
-
debounceMs: 1000,
|
|
119
|
-
documentId,
|
|
120
|
-
});
|
|
121
|
-
const versions = useMdKitDocumentVersions({ adapter, documentId });
|
|
122
|
-
|
|
123
|
-
const collaboration = useMdKitCollaboration({
|
|
124
|
-
collaborator: { id: "user-1", name: "Ada" },
|
|
125
|
-
documentId,
|
|
126
|
-
endpoint: `${apiUrl.replace(/^http/, "ws")}/collaboration`,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
const restoreVersion = async (version: MdKitDocumentVersionDetail) => {
|
|
130
|
-
await trpc.mdkit.restoreDocumentVersion.mutate({
|
|
131
|
-
documentId,
|
|
132
|
-
versionId: version.id,
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
await document.resync();
|
|
136
|
-
await versions.refresh();
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<>
|
|
141
|
-
<MdKitDocumentToolbar
|
|
142
|
-
collaboration={collaboration}
|
|
143
|
-
document={document}
|
|
144
|
-
versions={versions}
|
|
145
|
-
onOpenConflict={() => setConflictOpen(true)}
|
|
146
|
-
onOpenVersionHistory={() => setVersionHistoryOpen(true)}
|
|
147
|
-
/>
|
|
148
|
-
|
|
149
|
-
<MdKitEditor
|
|
150
|
-
collaboration={collaboration}
|
|
151
|
-
fillHeight
|
|
152
|
-
readOnly={document.conflict}
|
|
153
|
-
value={document.value}
|
|
154
|
-
onChange={document.setContent}
|
|
155
|
-
onFocusChange={document.setFocused}
|
|
156
|
-
/>
|
|
157
|
-
|
|
158
|
-
{versionHistoryOpen ? (
|
|
159
|
-
<div role="dialog" aria-label="Version history">
|
|
160
|
-
<VersionHistoryPanel
|
|
161
|
-
controller={versions}
|
|
162
|
-
onRestoreVersion={restoreVersion}
|
|
163
|
-
/>
|
|
164
|
-
</div>
|
|
165
|
-
) : null}
|
|
166
|
-
|
|
167
|
-
{document.conflict && conflictOpen ? (
|
|
168
|
-
<div role="dialog" aria-label="Resolve conflict">
|
|
169
|
-
<MdKitConflictPanel document={document} />
|
|
170
|
-
</div>
|
|
171
|
-
) : null}
|
|
172
|
-
</>
|
|
173
|
-
);
|
|
174
|
-
}
|
|
175
|
-
```
|
|
176
|
-
|
|
177
|
-
The base panels are starter UI; if they do not fit your product, keep the hooks
|
|
178
|
-
and build your own workflow components.
|
|
179
|
-
|
|
180
|
-
### Backend Store Contract
|
|
181
|
-
|
|
182
|
-
The backend starts with a store object. Replace `createYourDocumentStore()` with
|
|
183
|
-
Postgres, MongoDB, Redis, files, or any other durable storage. Your store
|
|
184
|
-
implements the [`MdKitBackendStore`](./api.md#mdkitbackendstore) interface:
|
|
185
|
-
read/write the current document, create/read checkpoints, restore a checkpoint,
|
|
186
|
-
and optionally persist collaboration state.
|
|
187
|
-
The mdkit backend helper applies checkpoint policy and turns those primitives
|
|
188
|
-
into tRPC or REST procedures. Application-owned metadata, auth, permissions,
|
|
189
|
-
tenancy, and durable Yjs storage stay in your code; see
|
|
190
|
-
[Permissions](./permissions.md).
|
|
191
|
-
|
|
192
|
-
### Backend With tRPC
|
|
193
|
-
|
|
194
|
-
```ts
|
|
195
|
-
import { createHTTPServer } from "@trpc/server/adapters/standalone";
|
|
196
|
-
import { Database } from "@hocuspocus/extension-database";
|
|
197
|
-
import { Server } from "@hocuspocus/server";
|
|
198
|
-
import { WebSocketServer } from "ws";
|
|
199
|
-
import { CheckpointPolicy } from "@mp-lb/mdkit/core";
|
|
200
|
-
import { t } from "./trpc";
|
|
201
|
-
import {
|
|
202
|
-
createMdKitBackend,
|
|
203
|
-
type MdKitBackendStore,
|
|
204
|
-
} from "@mp-lb/mdkit/server";
|
|
205
|
-
import { createMdKitTrpcRouter } from "@mp-lb/mdkit/trpc/server";
|
|
206
|
-
|
|
207
|
-
const store: MdKitBackendStore = createYourDocumentStore();
|
|
208
|
-
|
|
209
|
-
const mdkit = createMdKitBackend({
|
|
210
|
-
store,
|
|
211
|
-
checkpointPolicy: CheckpointPolicy.smart(),
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
const collaboration = Server.configure({
|
|
215
|
-
extensions: [
|
|
216
|
-
new Database({
|
|
217
|
-
fetch: ({ documentName }) => mdkit.readCollaborationState(documentName),
|
|
218
|
-
store: ({ documentName, state }) =>
|
|
219
|
-
mdkit.writeCollaborationState(documentName, state),
|
|
220
|
-
}),
|
|
221
|
-
],
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
const appRouter = t.router({
|
|
225
|
-
mdkit: createMdKitTrpcRouter(mdkit),
|
|
226
|
-
// otherRouters: ...
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
export type AppRouter = typeof appRouter;
|
|
230
|
-
|
|
231
|
-
const server = createHTTPServer({
|
|
232
|
-
basePath: "/trpc",
|
|
233
|
-
router: appRouter,
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
const collaborationSockets = new WebSocketServer({ noServer: true });
|
|
237
|
-
|
|
238
|
-
collaborationSockets.on("connection", (socket, request) => {
|
|
239
|
-
collaboration.handleConnection(socket, request, {});
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
server.on("upgrade", (request, socket, head) => {
|
|
243
|
-
if (!request.url?.startsWith("/collaboration")) {
|
|
244
|
-
socket.destroy();
|
|
245
|
-
return;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
collaborationSockets.handleUpgrade(request, socket, head, (websocket) => {
|
|
249
|
-
collaborationSockets.emit("connection", websocket, request);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
server.listen(Number(process.env.PORT ?? 4312));
|
|
254
|
-
```
|
|
255
|
-
|
|
256
|
-
**Important:** if you enable collaboration, decide where Hocuspocus Yjs state is
|
|
257
|
-
stored; in-memory state is simple but does not scale across backend instances.
|
|
258
|
-
See [Collaboration Persistence](./collaboration-persistence.md).
|
|
259
|
-
|
|
260
|
-
If the frontend runs on a different origin, put this server behind your app's
|
|
261
|
-
dev proxy or add CORS handling around the tRPC handler. See
|
|
262
|
-
[API Reference](./api.md#transport-helpers) for the full backend helper API.
|
|
263
|
-
|
|
264
|
-
### REST
|
|
265
|
-
|
|
266
|
-
The quick start uses tRPC because it gives the connected editor a complete typed
|
|
267
|
-
backend surface. REST is supported, but it is more verbose: the frontend adapter
|
|
268
|
-
expects matching REST endpoints, and restore currently needs one explicit
|
|
269
|
-
request from your UI.
|
|
270
|
-
|
|
271
|
-
See [REST Backend](./rest.md) for the backend route shape and the small
|
|
272
|
-
frontend transport changes.
|
|
273
|
-
|
|
274
|
-
### Checkpoints
|
|
275
|
-
|
|
276
|
-
Checkpoint policies decide when saved content becomes history. The backend
|
|
277
|
-
example uses `CheckpointPolicy.smart()`, which applies mdkit's default
|
|
278
|
-
autosave-friendly policy. See [Checkpoint Policy](./api.md#checkpoint-policy)
|
|
279
|
-
for `never`, `always`, `smart`, and custom policy functions.
|
|
280
|
-
|
|
281
|
-
## Questions
|
|
282
|
-
|
|
283
|
-
### Do the backends need to know about each other?
|
|
284
|
-
|
|
285
|
-
Storage, checkpoint history, and collaboration can stay separate. Storage stores
|
|
286
|
-
the current markdown snapshot. Checkpoint history stores immutable markdown
|
|
287
|
-
snapshots. Hocuspocus hosts live Yjs collaboration state, and your application
|
|
288
|
-
chooses whether and where to persist that state. These pieces need glue when
|
|
289
|
-
your product wants collaborative edits to become canonical markdown or
|
|
290
|
-
checkpoints.
|
|
291
|
-
|
|
292
|
-
### Does mdkit require tRPC or these exact REST endpoints?
|
|
293
|
-
|
|
294
|
-
No. The frontend hooks only need an `MdKitDocumentAdapter`. You can use REST,
|
|
295
|
-
tRPC, GraphQL, server actions, IndexedDB, Rails, Go, or anything else as long as
|
|
296
|
-
your adapter returns the documented shapes.
|
|
297
|
-
|
|
298
|
-
### How do store callbacks access user or request context?
|
|
299
|
-
|
|
300
|
-
In the tRPC quick-start path, `createMdKitTrpcRouter(mdkit)` is the simple
|
|
301
|
-
version: create one mdkit backend around a store object, then mount the router.
|
|
302
|
-
Use that when the store can enforce permissions and write metadata without
|
|
303
|
-
per-request React editor context.
|
|
304
|
-
|
|
305
|
-
If your store methods need the authenticated user, organization, request id,
|
|
306
|
-
logger, or other trusted request state, create the store at your application
|
|
307
|
-
request boundary and close over that context:
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
const createStoreForRequest = (ctx: AppContext): MdKitBackendStore => ({
|
|
311
|
-
readDocument: (documentId) =>
|
|
312
|
-
files.readCurrentMarkdown({ documentId, user: ctx.user }),
|
|
313
|
-
|
|
314
|
-
writeDocument: (input) =>
|
|
315
|
-
files.writeCurrentMarkdown({
|
|
316
|
-
...input,
|
|
317
|
-
actorId: ctx.user.id,
|
|
318
|
-
}),
|
|
319
|
-
|
|
320
|
-
createCheckpoint: (input) =>
|
|
321
|
-
files.createMarkdownCheckpoint({
|
|
322
|
-
...input,
|
|
323
|
-
actorId: ctx.user.id,
|
|
324
|
-
}),
|
|
325
|
-
});
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
With tRPC, that means writing the mdkit procedures in your app router when you
|
|
329
|
-
need request-scoped behavior, and calling `createMdKitBackend({
|
|
330
|
-
store: createStoreForRequest(ctx), checkpointPolicy })` inside those
|
|
331
|
-
procedures. With REST, use the same pattern inside your route handlers after
|
|
332
|
-
you have authenticated the request. Mdkit does not need to understand your auth
|
|
333
|
-
model; it only needs store callbacks that can read, write, and checkpoint the
|
|
334
|
-
document.
|
|
335
|
-
|
|
336
|
-
### Do I have to use the base panels?
|
|
337
|
-
|
|
338
|
-
No. The base panels are the fastest way to get a complete workflow working in
|
|
339
|
-
any React app. If you want a fully custom UI, use `useMdKitDocument`,
|
|
340
|
-
`useMdKitDocumentVersions`, and `useMdKitCollaboration` directly and render your
|
|
341
|
-
own controls.
|
package/docs/permissions.md
DELETED
|
@@ -1,139 +0,0 @@
|
|
|
1
|
-
# Permissions
|
|
2
|
-
|
|
3
|
-
MDKit should make connected editing secure without owning your application's
|
|
4
|
-
auth system.
|
|
5
|
-
|
|
6
|
-
The application owns authentication, user identity, tenancy, document ownership,
|
|
7
|
-
roles, teams, audit logging, and permission policy. MDKit owns where permission
|
|
8
|
-
checks happen in the document, checkpoint, restore, and collaboration
|
|
9
|
-
lifecycle.
|
|
10
|
-
|
|
11
|
-
## Permission Contract
|
|
12
|
-
|
|
13
|
-
The proposed permission contract is one `can` function:
|
|
14
|
-
|
|
15
|
-
```ts
|
|
16
|
-
type MdKitPermissionAction = "read" | "write" | "restore";
|
|
17
|
-
|
|
18
|
-
type MdKitPermissions<User = unknown, Document = unknown, Context = unknown> = {
|
|
19
|
-
can(
|
|
20
|
-
action: MdKitPermissionAction,
|
|
21
|
-
user: User,
|
|
22
|
-
document: Document,
|
|
23
|
-
context: Context,
|
|
24
|
-
): Promise<boolean> | boolean;
|
|
25
|
-
};
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
This is intentionally small. Products that do not need a separate restore
|
|
29
|
-
permission can implement `restore` as equivalent to `write`.
|
|
30
|
-
|
|
31
|
-
## Action Semantics
|
|
32
|
-
|
|
33
|
-
`read` allows reading the current document and checkpoint history.
|
|
34
|
-
|
|
35
|
-
`write` allows changing the current document, creating checkpoints, joining a
|
|
36
|
-
collaboration room, and writing/snapshotting collaboration state. Collaboration
|
|
37
|
-
requires `write`; MDKit does not need read-only collaboration in the initial
|
|
38
|
-
permission model.
|
|
39
|
-
|
|
40
|
-
`restore` allows restoring a checkpoint into the current document and resetting
|
|
41
|
-
collaboration state as part of that restore.
|
|
42
|
-
|
|
43
|
-
Permissions should be mandatory for the opinionated backend helper. A package
|
|
44
|
-
can expose an explicit allow-all implementation for local development and
|
|
45
|
-
applications that intentionally handle permissions outside MDKit:
|
|
46
|
-
|
|
47
|
-
```ts
|
|
48
|
-
export const mdKitDisablePermissions: MdKitPermissions = {
|
|
49
|
-
can: () => true,
|
|
50
|
-
};
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
Disabling permission checks should be visible in code. It should not be the
|
|
54
|
-
accidental default.
|
|
55
|
-
|
|
56
|
-
## Collaboration Security
|
|
57
|
-
|
|
58
|
-
Collaboration room names are not secrets. The server must authorize every room
|
|
59
|
-
join.
|
|
60
|
-
|
|
61
|
-
Joining a collaboration room is both read access and write access to the live
|
|
62
|
-
Yjs document. Once connected, a client can receive live state and send Yjs
|
|
63
|
-
updates unless the server rejects or constrains that connection. Storage
|
|
64
|
-
callbacks alone are not enough because active Yjs state can be read or changed
|
|
65
|
-
before a persistence callback runs.
|
|
66
|
-
|
|
67
|
-
The join rule is:
|
|
68
|
-
|
|
69
|
-
```ts
|
|
70
|
-
can("write", user, document, context)
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
If permission fails, the WebSocket must be rejected before the client receives
|
|
74
|
-
document state or sends accepted Yjs updates.
|
|
75
|
-
|
|
76
|
-
## Enforcement Levels
|
|
77
|
-
|
|
78
|
-
Applications should be able to choose how strictly collaboration permissions are
|
|
79
|
-
enforced after a session joins.
|
|
80
|
-
|
|
81
|
-
### Join Only
|
|
82
|
-
|
|
83
|
-
Check `can("write")` when the user joins the collaboration room. This is simple
|
|
84
|
-
and common. Tradeoff: if permissions are revoked mid-session, the active socket
|
|
85
|
-
may keep editing until it disconnects, is closed, or is forced to reconnect.
|
|
86
|
-
WebSocket ping timeouts are liveness checks, not permission expiry.
|
|
87
|
-
|
|
88
|
-
### Per-Message Permission Check
|
|
89
|
-
|
|
90
|
-
Check `can("write")` in Hocuspocus `beforeHandleMessage` before every incoming
|
|
91
|
-
Yjs message is applied. This is closest to checking permissions before every
|
|
92
|
-
HTTP write request. It can read from the database directly, but it may be too
|
|
93
|
-
expensive for high-volume collaboration.
|
|
94
|
-
|
|
95
|
-
### Per-Message Revocation Check
|
|
96
|
-
|
|
97
|
-
Run full permission policy on join and permission-change operations, then check
|
|
98
|
-
a cheap revocation source per packet: an in-memory set, cache entry, permission
|
|
99
|
-
generation, or short-lived authorization record.
|
|
100
|
-
|
|
101
|
-
This is the practical compromise for stricter apps. The revocation source must
|
|
102
|
-
have the ordering guarantee the product needs. If a revocation request returns
|
|
103
|
-
success before every collaboration server can observe the revoked state, the
|
|
104
|
-
system has eventual revocation rather than strict post-response write
|
|
105
|
-
prevention.
|
|
106
|
-
|
|
107
|
-
## Hocuspocus Hooks
|
|
108
|
-
|
|
109
|
-
Hocuspocus exposes the server hooks MDKit needs:
|
|
110
|
-
|
|
111
|
-
- `onConnect` runs when a client first asks to connect to a document
|
|
112
|
-
- `onAuthenticate` runs when authentication is enabled and receives the provider
|
|
113
|
-
token
|
|
114
|
-
- `beforeHandleMessage` runs before an incoming collaboration message is applied
|
|
115
|
-
to the Yjs document
|
|
116
|
-
|
|
117
|
-
`useMdKitCollaboration` already has a token hook. That token is not the
|
|
118
|
-
permission policy; it is how the server identifies the user and request context
|
|
119
|
-
before calling `can`.
|
|
120
|
-
|
|
121
|
-
## Backend Helper Responsibilities
|
|
122
|
-
|
|
123
|
-
An opinionated MDKit backend helper should call permissions at every server-side
|
|
124
|
-
entry point:
|
|
125
|
-
|
|
126
|
-
| Operation | Required Action |
|
|
127
|
-
| --- | --- |
|
|
128
|
-
| Read current document | `read` |
|
|
129
|
-
| Write current document | `write` |
|
|
130
|
-
| List checkpoints | `read` |
|
|
131
|
-
| Read checkpoint | `read` |
|
|
132
|
-
| Create checkpoint | `write` |
|
|
133
|
-
| Restore checkpoint | `restore` |
|
|
134
|
-
| Join collaboration | `write` |
|
|
135
|
-
| Persist collaboration state from an authorized room | `write` |
|
|
136
|
-
| Reset collaboration state during restore | `restore` |
|
|
137
|
-
|
|
138
|
-
Frontend UI checks are convenience. Server-side checks are the security
|
|
139
|
-
boundary.
|