@mp-lb/mdkit 0.3.1 → 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.
Files changed (160) hide show
  1. package/README.md +8 -2
  2. package/dist/collaboration/useMdKitCollaboration.d.ts +5 -0
  3. package/dist/collaboration/useMdKitCollaboration.d.ts.map +1 -0
  4. package/dist/collaboration/useMdKitCollaboration.js +4 -0
  5. package/dist/core/checkpointPolicy.d.ts +10 -0
  6. package/dist/core/checkpointPolicy.d.ts.map +1 -0
  7. package/dist/core/checkpointPolicy.js +9 -0
  8. package/dist/core/documentEngine.d.ts +1 -0
  9. package/dist/core/documentEngine.d.ts.map +1 -0
  10. package/dist/core/index.d.ts +1 -0
  11. package/dist/core/index.d.ts.map +1 -0
  12. package/dist/document/MdKitConflictPanel.d.ts +5 -0
  13. package/dist/document/MdKitConflictPanel.d.ts.map +1 -0
  14. package/dist/document/MdKitConflictPanel.js +4 -0
  15. package/dist/document/MdKitDocumentToolbar.d.ts +6 -0
  16. package/dist/document/MdKitDocumentToolbar.d.ts.map +1 -0
  17. package/dist/document/MdKitDocumentToolbar.js +5 -0
  18. package/dist/document/documentTypes.d.ts +6 -0
  19. package/dist/document/documentTypes.d.ts.map +1 -0
  20. package/dist/document/useMdKitDocument.d.ts +5 -0
  21. package/dist/document/useMdKitDocument.d.ts.map +1 -0
  22. package/dist/document/useMdKitDocument.js +4 -0
  23. package/dist/fastify.d.ts +1 -0
  24. package/dist/fastify.d.ts.map +1 -0
  25. package/dist/index.d.ts +4 -1
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/markdown/MarkdownBubbleMenu.d.ts +1 -0
  28. package/dist/markdown/MarkdownBubbleMenu.d.ts.map +1 -0
  29. package/dist/markdown/MarkdownPasteExtension.d.ts +1 -0
  30. package/dist/markdown/MarkdownPasteExtension.d.ts.map +1 -0
  31. package/dist/markdown/MarkdownSearchExtension.d.ts +1 -0
  32. package/dist/markdown/MarkdownSearchExtension.d.ts.map +1 -0
  33. package/dist/markdown/MarkdownSearchPanel.d.ts +1 -0
  34. package/dist/markdown/MarkdownSearchPanel.d.ts.map +1 -0
  35. package/dist/markdown/MdKitEditor.d.ts +11 -0
  36. package/dist/markdown/MdKitEditor.d.ts.map +1 -0
  37. package/dist/markdown/MdKitEditor.js +10 -2
  38. package/dist/markdown/MdKitView.d.ts +9 -1
  39. package/dist/markdown/MdKitView.d.ts.map +1 -0
  40. package/dist/markdown/MdKitView.js +7 -2
  41. package/dist/markdown/TiptapMarkdownSurface.d.ts +1 -0
  42. package/dist/markdown/TiptapMarkdownSurface.d.ts.map +1 -0
  43. package/dist/markdown/TiptapMarkdownSurface.js +10 -22
  44. package/dist/markdown/createMdKitTiptapExtensions.d.ts +1 -0
  45. package/dist/markdown/createMdKitTiptapExtensions.d.ts.map +1 -0
  46. package/dist/markdown/editorDebug.d.ts +1 -0
  47. package/dist/markdown/editorDebug.d.ts.map +1 -0
  48. package/dist/markdown/markdownFenceRanges.d.ts +1 -0
  49. package/dist/markdown/markdownFenceRanges.d.ts.map +1 -0
  50. package/dist/markdown/normalizeMarkdownSerialization.d.ts +1 -0
  51. package/dist/markdown/normalizeMarkdownSerialization.d.ts.map +1 -0
  52. package/dist/markdown/prepareMarkdownForEditorHydration.d.ts +1 -0
  53. package/dist/markdown/prepareMarkdownForEditorHydration.d.ts.map +1 -0
  54. package/dist/markdown/preserveMarkdownWhitespace.d.ts +1 -0
  55. package/dist/markdown/preserveMarkdownWhitespace.d.ts.map +1 -0
  56. package/dist/markdown/yamlFrontMatter.d.ts +1 -0
  57. package/dist/markdown/yamlFrontMatter.d.ts.map +1 -0
  58. package/dist/server.d.ts +1 -0
  59. package/dist/server.d.ts.map +1 -0
  60. package/dist/theme/MdKitThemeEditor.d.ts +5 -0
  61. package/dist/theme/MdKitThemeEditor.d.ts.map +1 -0
  62. package/dist/theme/MdKitThemeEditor.js +4 -0
  63. package/dist/theme/editorTheme.d.ts +1 -0
  64. package/dist/theme/editorTheme.d.ts.map +1 -0
  65. package/dist/theme/editorTheme.js +8 -8
  66. package/dist/transport/backend.d.ts +13 -0
  67. package/dist/transport/backend.d.ts.map +1 -0
  68. package/dist/transport/backend.js +6 -0
  69. package/dist/transport/fastify.d.ts +5 -0
  70. package/dist/transport/fastify.d.ts.map +1 -0
  71. package/dist/transport/fastify.js +4 -0
  72. package/dist/transport/http.d.ts +1 -0
  73. package/dist/transport/http.d.ts.map +1 -0
  74. package/dist/transport/index.d.ts +1 -0
  75. package/dist/transport/index.d.ts.map +1 -0
  76. package/dist/transport/rest.d.ts +6 -0
  77. package/dist/transport/rest.d.ts.map +1 -0
  78. package/dist/transport/rest.js +5 -0
  79. package/dist/transport/store.d.ts +1 -0
  80. package/dist/transport/store.d.ts.map +1 -0
  81. package/dist/transport/trpcClient.d.ts +8 -0
  82. package/dist/transport/trpcClient.d.ts.map +1 -0
  83. package/dist/transport/trpcClient.js +7 -0
  84. package/dist/transport/trpcServer.d.ts +6 -0
  85. package/dist/transport/trpcServer.d.ts.map +1 -0
  86. package/dist/transport/trpcServer.js +5 -0
  87. package/dist/trpc/client.d.ts +1 -0
  88. package/dist/trpc/client.d.ts.map +1 -0
  89. package/dist/trpc/server.d.ts +1 -0
  90. package/dist/trpc/server.d.ts.map +1 -0
  91. package/dist/trpc.d.ts +1 -0
  92. package/dist/trpc.d.ts.map +1 -0
  93. package/dist/ui/joinClassNames.d.ts +1 -0
  94. package/dist/ui/joinClassNames.d.ts.map +1 -0
  95. package/dist/versioning/VersionHistoryPanel.d.ts +5 -0
  96. package/dist/versioning/VersionHistoryPanel.d.ts.map +1 -0
  97. package/dist/versioning/VersionHistoryPanel.js +4 -0
  98. package/dist/versioning/useMdKitDocumentVersions.d.ts +5 -0
  99. package/dist/versioning/useMdKitDocumentVersions.d.ts.map +1 -0
  100. package/dist/versioning/useMdKitDocumentVersions.js +4 -0
  101. package/dist/yjs/MdKitMarkdownYjs.d.ts +1 -0
  102. package/dist/yjs/MdKitMarkdownYjs.d.ts.map +1 -0
  103. package/dist/yjs/index.d.ts +1 -0
  104. package/dist/yjs/index.d.ts.map +1 -0
  105. package/package.json +10 -12
  106. package/src/collaboration/useMdKitCollaboration.ts +528 -0
  107. package/src/core/checkpointPolicy.ts +107 -0
  108. package/src/core/documentEngine.ts +175 -0
  109. package/src/core/index.ts +33 -0
  110. package/src/document/MdKitConflictPanel.tsx +129 -0
  111. package/src/document/MdKitDocumentToolbar.tsx +141 -0
  112. package/src/document/documentTypes.ts +89 -0
  113. package/src/document/useMdKitDocument.ts +543 -0
  114. package/src/fastify.ts +6 -0
  115. package/src/index.ts +89 -0
  116. package/src/markdown/MarkdownBubbleMenu.tsx +271 -0
  117. package/src/markdown/MarkdownPasteExtension.ts +81 -0
  118. package/src/markdown/MarkdownSearchExtension.ts +77 -0
  119. package/src/markdown/MarkdownSearchPanel.tsx +98 -0
  120. package/src/markdown/MdKitEditor.tsx +75 -0
  121. package/src/markdown/MdKitView.tsx +80 -0
  122. package/src/markdown/TiptapMarkdownSurface.tsx +923 -0
  123. package/src/markdown/createMdKitTiptapExtensions.ts +42 -0
  124. package/src/markdown/editorDebug.ts +5 -0
  125. package/src/markdown/markdownFenceRanges.ts +68 -0
  126. package/src/markdown/normalizeMarkdownSerialization.ts +55 -0
  127. package/src/markdown/prepareMarkdownForEditorHydration.ts +23 -0
  128. package/src/markdown/preserveMarkdownWhitespace.ts +143 -0
  129. package/src/markdown/yamlFrontMatter.ts +135 -0
  130. package/src/server.ts +6 -0
  131. package/src/styles.css +125 -53
  132. package/src/theme/MdKitThemeEditor.tsx +134 -0
  133. package/src/theme/editorTheme.ts +72 -0
  134. package/src/transport/backend.ts +220 -0
  135. package/src/transport/fastify.ts +57 -0
  136. package/src/transport/http.ts +126 -0
  137. package/src/transport/index.ts +12 -0
  138. package/src/transport/rest.ts +80 -0
  139. package/src/transport/store.ts +45 -0
  140. package/src/transport/trpcClient.ts +90 -0
  141. package/src/transport/trpcServer.ts +66 -0
  142. package/src/trpc/client.ts +11 -0
  143. package/src/trpc/server.ts +12 -0
  144. package/src/trpc.ts +11 -0
  145. package/src/ui/joinClassNames.ts +3 -0
  146. package/src/versioning/VersionHistoryPanel.tsx +146 -0
  147. package/src/versioning/useMdKitDocumentVersions.ts +146 -0
  148. package/src/yjs/MdKitMarkdownYjs.ts +111 -0
  149. package/src/yjs/index.ts +8 -0
  150. package/docs/.vitepress/config.ts +0 -47
  151. package/docs/api.md +0 -512
  152. package/docs/architecture.md +0 -96
  153. package/docs/collaboration-persistence.md +0 -147
  154. package/docs/index.md +0 -341
  155. package/docs/permissions.md +0 -139
  156. package/docs/plain-text.md +0 -131
  157. package/docs/rest.md +0 -98
  158. package/docs/shadcn.md +0 -125
  159. package/docs/styling.md +0 -373
  160. 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.
@@ -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.