deepspace 0.10.1 → 0.10.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 (75) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/chunk-ZJ43EK3N.js +1751 -0
  3. package/dist/chunk-ZJ43EK3N.js.map +1 -0
  4. package/dist/cli.js +848 -650
  5. package/dist/cli.js.map +1 -1
  6. package/dist/documentation-client-core.d.ts +18 -0
  7. package/dist/documentation-client-core.js +395 -0
  8. package/dist/documentation-client-core.js.map +1 -0
  9. package/dist/documentation-default-renderer.cjs +124 -0
  10. package/dist/documentation-react.d.ts +2 -0
  11. package/dist/documentation-react.js +35 -0
  12. package/dist/documentation-react.js.map +1 -0
  13. package/dist/documentation-runtime.js +71 -0
  14. package/dist/documentation-server-core.d.ts +11 -0
  15. package/dist/documentation-server-core.js +48 -0
  16. package/dist/documentation-server-core.js.map +1 -0
  17. package/dist/documentation.d.ts +367 -0
  18. package/dist/documentation.js +4122 -0
  19. package/dist/documentation.js.map +1 -0
  20. package/dist/feature-installer.js +145 -0
  21. package/dist/feature-installer.js.map +1 -1
  22. package/dist/fonts/geist-mono-OFL.txt +93 -0
  23. package/dist/fonts/geist-mono-variable.woff2 +0 -0
  24. package/dist/fonts/inter-OFL.txt +92 -0
  25. package/dist/fonts/inter-variable.woff2 +0 -0
  26. package/dist/index.d.ts +177 -1
  27. package/dist/index.js +133 -0
  28. package/dist/index.js.map +1 -1
  29. package/dist/public-BGRX_Cjd.d.ts +234 -0
  30. package/dist/schema.d.ts +186 -0
  31. package/dist/schema.js +610 -0
  32. package/dist/schema.js.map +1 -0
  33. package/dist/server.d.ts +83 -4
  34. package/dist/server.js +296 -1
  35. package/dist/server.js.map +1 -1
  36. package/dist/testing.d.ts +5 -8
  37. package/dist/testing.js +48 -14
  38. package/dist/testing.js.map +1 -1
  39. package/dist/worker.d.ts +249 -5
  40. package/dist/worker.js +1438 -19
  41. package/dist/worker.js.map +1 -1
  42. package/features/admin-page/src/admin-schema.ts +1 -1
  43. package/features/ai-chat/feature.json +2 -2
  44. package/features/ai-chat/src/ChatPanel.tsx +6 -11
  45. package/features/ai-chat/src/ai-chat-schema.ts +1 -2
  46. package/features/canvas/src/canvas-schema.ts +1 -1
  47. package/features/documentation/content/index.mdx +14 -0
  48. package/features/documentation/content/quickstart.mdx +12 -0
  49. package/features/documentation/documentation.json +18 -0
  50. package/features/documentation/feature.json +68 -0
  51. package/features/{docs → documents}/feature.json +27 -27
  52. package/features/{docs/src/DocsEditorPage.tsx → documents/src/DocumentsEditorPage.tsx} +74 -74
  53. package/features/{docs/src/DocsListPage.tsx → documents/src/DocumentsListPage.tsx} +102 -102
  54. package/features/{docs/src/DocsOutlinePanel.tsx → documents/src/DocumentsOutlinePanel.tsx} +9 -9
  55. package/features/{docs/src/DocsPresence.tsx → documents/src/DocumentsPresence.tsx} +18 -18
  56. package/features/{docs → documents}/src/InviteDialog.tsx +21 -21
  57. package/features/{docs → documents}/src/LibrarySidebar.tsx +25 -25
  58. package/features/documents/src/__tests__/documents-presence-access.test.ts +71 -0
  59. package/features/{docs/src/docs-library-types.ts → documents/src/documents-library-types.ts} +3 -3
  60. package/features/{docs/src/docs-schema.ts → documents/src/documents-schema.ts} +6 -6
  61. package/features/{docs/src/docs-ui.css → documents/src/documents-ui.css} +89 -89
  62. package/features/{docs → documents}/src/editor/DocEditorSurface.tsx +11 -11
  63. package/features/{docs/src/editor/DocsTiptapToolbar.tsx → documents/src/editor/DocumentsTiptapToolbar.tsx} +21 -21
  64. package/features/{docs → documents}/src/editor/useDocEditor.ts +4 -4
  65. package/features/{docs/src/use-docs-presence-access.ts → documents/src/use-documents-presence-access.ts} +51 -51
  66. package/features/file-attachments/src/attachments-schema.ts +1 -1
  67. package/features/items/src/items-schema.ts +1 -1
  68. package/features/leaderboard/src/leaderboard-schema.ts +1 -1
  69. package/features/messaging/src/ChatMultiPage.tsx +1 -1
  70. package/features/messaging/src/ChatPage.tsx +1 -1
  71. package/features/messaging/src/messaging-schema.ts +2 -2
  72. package/features/tasks/src/tasks-schema.ts +1 -1
  73. package/features/testing/src/test-schema.ts +2 -2
  74. package/package.json +26 -1
  75. package/features/docs/src/__tests__/docs-presence-access.test.ts +0 -71
@@ -1,22 +1,22 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
2
2
  import { usePresenceRoom, type PresencePeerClient, type UsePresenceRoomResult } from 'deepspace'
3
3
  import type { Editor } from '@tiptap/react'
4
- import type { DocsPresenceParticipant } from './DocsPresence'
5
- import { parseDocsIdList, type DocsDocumentFields, type InviteAclDiff } from './docs-library-types'
4
+ import type { DocumentsPresenceParticipant } from './DocumentsPresence'
5
+ import { parseDocumentsIdList, type DocumentsDocumentFields, type InviteAclDiff } from './documents-library-types'
6
6
 
7
7
  const TYPING_IDLE_MS = 1600
8
8
  const TYPING_STALE_MS = 5000
9
9
  /** Re-broadcast presence so clients who connect later still see existing viewers. */
10
10
  const PRESENCE_HEARTBEAT_MS = 25_000
11
11
 
12
- export type DocsAccessRole = 'owner' | 'editor' | 'viewer' | 'none'
12
+ export type DocumentsAccessRole = 'owner' | 'editor' | 'viewer' | 'none'
13
13
  export type AccessChangeKind = 'downgrade' | 'upgrade' | 'revoked'
14
14
 
15
- interface DocsAccessDocument {
16
- data: Pick<DocsDocumentFields, 'ownerId' | 'collaborators' | 'editors'>
15
+ interface DocumentsAccessDocument {
16
+ data: Pick<DocumentsDocumentFields, 'ownerId' | 'collaborators' | 'editors'>
17
17
  }
18
18
 
19
- interface DocsPresenceUser {
19
+ interface DocumentsPresenceUser {
20
20
  id: string
21
21
  name?: string | null
22
22
  email?: string | null
@@ -28,27 +28,27 @@ interface DetectedAclEvent {
28
28
  at: number
29
29
  }
30
30
 
31
- export function isDocsOwner(
32
- document: DocsAccessDocument | null | undefined,
31
+ export function isDocumentsOwner(
32
+ document: DocumentsAccessDocument | null | undefined,
33
33
  userId: string | null | undefined,
34
34
  ): boolean {
35
35
  return Boolean(document && userId && document.data.ownerId === userId)
36
36
  }
37
37
 
38
- export function resolveDocsAccessRole(
39
- document: DocsAccessDocument | null | undefined,
38
+ export function resolveDocumentsAccessRole(
39
+ document: DocumentsAccessDocument | null | undefined,
40
40
  userId: string | null | undefined,
41
- ): DocsAccessRole {
41
+ ): DocumentsAccessRole {
42
42
  if (!userId || !document) return 'none'
43
- if (isDocsOwner(document, userId)) return 'owner'
44
- if (parseDocsIdList(document.data.editors).includes(userId)) return 'editor'
45
- if (parseDocsIdList(document.data.collaborators).includes(userId)) return 'viewer'
43
+ if (isDocumentsOwner(document, userId)) return 'owner'
44
+ if (parseDocumentsIdList(document.data.editors).includes(userId)) return 'editor'
45
+ if (parseDocumentsIdList(document.data.collaborators).includes(userId)) return 'viewer'
46
46
  return 'none'
47
47
  }
48
48
 
49
- export function deriveDocsAccessChange(
50
- initialRole: DocsAccessRole | null,
51
- effectiveRole: DocsAccessRole,
49
+ export function deriveDocumentsAccessChange(
50
+ initialRole: DocumentsAccessRole | null,
51
+ effectiveRole: DocumentsAccessRole,
52
52
  detectedEvent: DetectedAclEvent | null,
53
53
  ): AccessChangeKind | null {
54
54
  // A presence-derived revoke is the only signal guaranteed to reach a peer
@@ -64,7 +64,7 @@ export function deriveDocsAccessChange(
64
64
  return detectedEvent?.kind ?? null
65
65
  }
66
66
 
67
- export function detectDocsAclEvent(
67
+ export function detectDocumentsAclEvent(
68
68
  state: Record<string, unknown>,
69
69
  userId: string,
70
70
  sessionStartedAt: number,
@@ -89,11 +89,11 @@ export function detectDocsAclEvent(
89
89
  return null
90
90
  }
91
91
 
92
- function peerToDocsParticipant(peer: PresencePeerClient): DocsPresenceParticipant {
92
+ function peerToDocumentsParticipant(peer: PresencePeerClient): DocumentsPresenceParticipant {
93
93
  const state = peer.state
94
- const mode: DocsPresenceParticipant['mode'] = state.mode === 'view' ? 'view' : 'edit'
94
+ const mode: DocumentsPresenceParticipant['mode'] = state.mode === 'view' ? 'view' : 'edit'
95
95
  const lastTypedAt = typeof state.lastTypedAt === 'number' ? state.lastTypedAt : undefined
96
- const participant: DocsPresenceParticipant = {
96
+ const participant: DocumentsPresenceParticipant = {
97
97
  clientId: 0,
98
98
  userId: peer.userId,
99
99
  name: peer.userName?.trim() || peer.userEmail?.trim() || 'Guest',
@@ -107,25 +107,25 @@ function peerToDocsParticipant(peer: PresencePeerClient): DocsPresenceParticipan
107
107
  return participant
108
108
  }
109
109
 
110
- function sortDocsPresenceParticipants(
111
- left: DocsPresenceParticipant,
112
- right: DocsPresenceParticipant,
110
+ function sortDocumentsPresenceParticipants(
111
+ left: DocumentsPresenceParticipant,
112
+ right: DocumentsPresenceParticipant,
113
113
  ): number {
114
114
  if (left.isSelf !== right.isSelf) return left.isSelf ? -1 : 1
115
115
  if (left.mode !== right.mode) return left.mode === 'edit' ? -1 : 1
116
116
  return left.name.localeCompare(right.name)
117
117
  }
118
118
 
119
- function buildDocsPresence(
119
+ function buildDocumentsPresence(
120
120
  peers: PresencePeerClient[],
121
- self: DocsPresenceUser | null | undefined,
121
+ self: DocumentsPresenceUser | null | undefined,
122
122
  editMode: boolean,
123
- ): DocsPresenceParticipant[] {
123
+ ): DocumentsPresenceParticipant[] {
124
124
  if (!self) {
125
- return [...peers.map(peerToDocsParticipant)].sort(sortDocsPresenceParticipants)
125
+ return [...peers.map(peerToDocumentsParticipant)].sort(sortDocumentsPresenceParticipants)
126
126
  }
127
127
 
128
- const selfRow: DocsPresenceParticipant = {
128
+ const selfRow: DocumentsPresenceParticipant = {
129
129
  clientId: 0,
130
130
  userId: self.id,
131
131
  name: self.name?.trim() || self.email?.trim() || 'You',
@@ -135,21 +135,21 @@ function buildDocsPresence(
135
135
  if (self.email) selfRow.email = self.email
136
136
  if (self.imageUrl) selfRow.imageUrl = self.imageUrl
137
137
 
138
- const others = peers.filter((peer) => peer.userId !== self.id).map(peerToDocsParticipant)
139
- return [selfRow, ...others].sort(sortDocsPresenceParticipants)
138
+ const others = peers.filter((peer) => peer.userId !== self.id).map(peerToDocumentsParticipant)
139
+ return [selfRow, ...others].sort(sortDocumentsPresenceParticipants)
140
140
  }
141
141
 
142
- interface UseDocsPresenceAccessOptions {
142
+ interface UseDocumentsPresenceAccessOptions {
143
143
  docId: string | undefined
144
- document: DocsAccessDocument | null | undefined
145
- user: DocsPresenceUser | null | undefined
144
+ document: DocumentsAccessDocument | null | undefined
145
+ user: DocumentsPresenceUser | null | undefined
146
146
  yjsCanWrite: boolean
147
147
  writeAuthResolved: boolean
148
148
  }
149
149
 
150
- interface DocsPresenceAccessResult {
150
+ interface DocumentsPresenceAccessResult {
151
151
  isOwner: boolean
152
- effectiveRole: DocsAccessRole
152
+ effectiveRole: DocumentsAccessRole
153
153
  effectiveCanWrite: boolean
154
154
  showReadOnlyDocUx: boolean
155
155
  accessChangeKind: AccessChangeKind | null
@@ -158,21 +158,21 @@ interface DocsPresenceAccessResult {
158
158
  presenceRoom: UsePresenceRoomResult
159
159
  }
160
160
 
161
- export function useDocsPresenceAccess({
161
+ export function useDocumentsPresenceAccess({
162
162
  docId,
163
163
  document,
164
164
  user,
165
165
  yjsCanWrite,
166
166
  writeAuthResolved,
167
- }: UseDocsPresenceAccessOptions): DocsPresenceAccessResult {
167
+ }: UseDocumentsPresenceAccessOptions): DocumentsPresenceAccessResult {
168
168
  const presenceRoom = usePresenceRoom(docId ? `doc:${docId}` : '_')
169
169
  const { peers, updateState } = presenceRoom
170
- const isOwner = isDocsOwner(document, user?.id)
171
- const effectiveRole = resolveDocsAccessRole(document, user?.id)
170
+ const isOwner = isDocumentsOwner(document, user?.id)
171
+ const effectiveRole = resolveDocumentsAccessRole(document, user?.id)
172
172
  const policyCanEdit = effectiveRole === 'owner' || effectiveRole === 'editor'
173
173
 
174
174
  /** The first concrete role is the baseline for live ACL transitions. */
175
- const [initialRole, setInitialRole] = useState<DocsAccessRole | null>(null)
175
+ const [initialRole, setInitialRole] = useState<DocumentsAccessRole | null>(null)
176
176
  const sessionStartedAtRef = useRef<number | null>(null)
177
177
  useEffect(() => {
178
178
  setInitialRole(null)
@@ -196,12 +196,12 @@ export function useDocsPresenceAccess({
196
196
 
197
197
  const ownerPeer = peers.find((peer) => peer.userId === document.data.ownerId)
198
198
  if (!ownerPeer) return
199
- const nextEvent = detectDocsAclEvent(ownerPeer.state, user.id, sessionStartedAt)
199
+ const nextEvent = detectDocumentsAclEvent(ownerPeer.state, user.id, sessionStartedAt)
200
200
  if (nextEvent) setDetectedAclEvent(nextEvent)
201
201
  }, [detectedAclEvent, document, peers, user])
202
202
 
203
203
  const accessChangeKind = useMemo(
204
- () => deriveDocsAccessChange(initialRole, effectiveRole, detectedAclEvent),
204
+ () => deriveDocumentsAccessChange(initialRole, effectiveRole, detectedAclEvent),
205
205
  [detectedAclEvent, effectiveRole, initialRole],
206
206
  )
207
207
  const accessLocked = accessChangeKind !== null
@@ -239,22 +239,22 @@ export function useDocsPresenceAccess({
239
239
  }
240
240
  }
241
241
 
242
- interface UseDocsEditorPresenceOptions {
242
+ interface UseDocumentsEditorPresenceOptions {
243
243
  editor: Editor | null
244
- document: DocsAccessDocument | null | undefined
245
- user: DocsPresenceUser | null | undefined
244
+ document: DocumentsAccessDocument | null | undefined
245
+ user: DocumentsPresenceUser | null | undefined
246
246
  synced: boolean
247
- access: DocsPresenceAccessResult
247
+ access: DocumentsPresenceAccessResult
248
248
  }
249
249
 
250
- export function useDocsEditorPresence({
250
+ export function useDocumentsEditorPresence({
251
251
  editor,
252
252
  document,
253
253
  user,
254
254
  synced,
255
255
  access,
256
- }: UseDocsEditorPresenceOptions): {
257
- participants: DocsPresenceParticipant[]
256
+ }: UseDocumentsEditorPresenceOptions): {
257
+ participants: DocumentsPresenceParticipant[]
258
258
  typingNames: string[]
259
259
  } {
260
260
  const { effectiveCanWrite, presenceAsEditMode, presenceRoom } = access
@@ -263,7 +263,7 @@ export function useDocsEditorPresence({
263
263
  const typingTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
264
264
 
265
265
  const participants = useMemo(
266
- () => buildDocsPresence(peers, user, presenceAsEditMode),
266
+ () => buildDocumentsPresence(peers, user, presenceAsEditMode),
267
267
  [peers, presenceAsEditMode, user],
268
268
  )
269
269
 
@@ -10,7 +10,7 @@
10
10
  * - Pairing collection metadata with R2 file storage
11
11
  */
12
12
 
13
- import type { CollectionSchema } from 'deepspace/worker'
13
+ import type { CollectionSchema } from 'deepspace/schema'
14
14
 
15
15
  export const attachmentsSchema: CollectionSchema = {
16
16
  name: 'attachments',
@@ -8,7 +8,7 @@
8
8
  * - Basic CRUD with role-based permissions
9
9
  */
10
10
 
11
- import type { CollectionSchema } from 'deepspace/worker'
11
+ import type { CollectionSchema } from 'deepspace/schema'
12
12
 
13
13
  export const itemsSchema: CollectionSchema = {
14
14
  name: 'items',
@@ -7,7 +7,7 @@
7
7
  * - Admin override for updating any entry
8
8
  */
9
9
 
10
- import type { CollectionSchema } from 'deepspace/worker'
10
+ import type { CollectionSchema } from 'deepspace/schema'
11
11
 
12
12
  export const leaderboardSchema: CollectionSchema = {
13
13
  name: 'leaderboard',
@@ -19,7 +19,7 @@ import { useMutations } from 'deepspace'
19
19
  import { RecordScope } from 'deepspace'
20
20
  import type { Channel, ChannelMember } from 'deepspace'
21
21
  import type { RecordData } from 'deepspace'
22
- import type { CollectionSchema } from 'deepspace/worker'
22
+ import type { CollectionSchema } from 'deepspace/schema'
23
23
  import { messagingSchemas } from '../schemas/messaging-schema'
24
24
  import { ChannelSidebar } from '../components/messaging/chat-multi/ChannelSidebar'
25
25
  import { MessageList } from '../components/messaging/chat/MessageList'
@@ -7,7 +7,7 @@ import { useEffect, useRef } from 'react'
7
7
  import { useChatChannel } from '../components/messaging/hooks/useChatChannel'
8
8
  import { useReadReceipts } from 'deepspace'
9
9
  import { RecordScope } from 'deepspace'
10
- import type { CollectionSchema } from 'deepspace/worker'
10
+ import type { CollectionSchema } from 'deepspace/schema'
11
11
  import { messagingSchemas } from '../schemas/messaging-schema'
12
12
  import { ChatHeader } from '../components/messaging/chat/ChatHeader'
13
13
  import { MessageList } from '../components/messaging/chat/MessageList'
@@ -5,7 +5,7 @@
5
5
  * Spread into the app's schemas array: ...messagingSchemas
6
6
  */
7
7
 
8
- import type { CollectionSchema } from 'deepspace/worker'
8
+ import type { CollectionSchema } from 'deepspace/schema'
9
9
  import {
10
10
  CHANNELS_SCHEMA,
11
11
  MESSAGES_SCHEMA,
@@ -13,7 +13,7 @@ import {
13
13
  CHANNEL_MEMBERS_SCHEMA,
14
14
  CHANNEL_INVITATIONS_SCHEMA,
15
15
  READ_RECEIPTS_SCHEMA,
16
- } from 'deepspace/worker'
16
+ } from 'deepspace/schema'
17
17
 
18
18
  export const messagingSchemas: CollectionSchema[] = [
19
19
  CHANNELS_SCHEMA,
@@ -8,7 +8,7 @@
8
8
  * - Admin-only grading workflow
9
9
  */
10
10
 
11
- import type { CollectionSchema } from 'deepspace/worker'
11
+ import type { CollectionSchema } from 'deepspace/schema'
12
12
 
13
13
  export const challengesSchema: CollectionSchema = {
14
14
  name: 'challenges',
@@ -2,8 +2,8 @@
2
2
  * Test Items Schema — exercises RBAC and CRUD for e2e testing.
3
3
  */
4
4
 
5
- import type { CollectionSchema } from 'deepspace/worker'
6
- import { ROLE_ANONYMOUS } from 'deepspace/worker'
5
+ import { ROLE_ANONYMOUS } from 'deepspace'
6
+ import type { CollectionSchema } from 'deepspace/schema'
7
7
 
8
8
  export const testItemsSchema: CollectionSchema = {
9
9
  name: 'test-items',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepspace",
3
- "version": "0.10.1",
3
+ "version": "0.10.3",
4
4
  "engines": {
5
5
  "node": ">=22.15.0"
6
6
  },
@@ -17,6 +17,10 @@
17
17
  "types": "./dist/index.d.ts",
18
18
  "import": "./dist/index.js"
19
19
  },
20
+ "./schema": {
21
+ "types": "./dist/schema.d.ts",
22
+ "import": "./dist/schema.js"
23
+ },
20
24
  "./worker": {
21
25
  "types": "./dist/worker.d.ts",
22
26
  "import": "./dist/worker.js"
@@ -28,6 +32,14 @@
28
32
  "./testing": {
29
33
  "types": "./dist/testing.d.ts",
30
34
  "import": "./dist/testing.js"
35
+ },
36
+ "./documentation": {
37
+ "types": "./dist/documentation.d.ts",
38
+ "import": "./dist/documentation.js"
39
+ },
40
+ "./documentation/react": {
41
+ "types": "./dist/documentation-react.d.ts",
42
+ "import": "./dist/documentation-react.js"
31
43
  }
32
44
  },
33
45
  "files": [
@@ -39,6 +51,7 @@
39
51
  "build": "node ../../scripts/clean-artifacts.mjs dist --tsbuildinfo && tsup",
40
52
  "type-check": "tsc --noEmit",
41
53
  "test": "vitest run",
54
+ "test:documentation-browser": "playwright test --config src/documentation/runtime/__browser__/playwright.config.ts",
42
55
  "clean": "node ../../scripts/clean-artifacts.mjs dist --tsbuildinfo",
43
56
  "prepublishOnly": "pnpm build"
44
57
  },
@@ -47,14 +60,25 @@
47
60
  "@ai-sdk/openai": "^2.0.0",
48
61
  "@ai-sdk/openai-compatible": "^1.0.0",
49
62
  "@clack/prompts": "^1.2.0",
63
+ "@mdx-js/mdx": "^3.1.1",
64
+ "acorn": "^8.16.0",
50
65
  "ai": "^5.0.0",
51
66
  "better-auth": "1.6.23",
52
67
  "citty": "^0.2.0",
53
68
  "cross-spawn": "^7.0.6",
54
69
  "esbuild": "^0.27.0",
70
+ "highlight.js": "^11.11.1",
55
71
  "hono": "^4.6.0",
56
72
  "jose": "^6.1.0",
73
+ "marked": "^18.0.7",
74
+ "mdast-util-gfm": "^3.1.0",
75
+ "mdast-util-to-markdown": "^2.1.2",
76
+ "rehype-highlight": "^7.0.2",
77
+ "remark-frontmatter": "^5.0.0",
78
+ "remark-gfm": "^4.0.1",
79
+ "sanitize-html": "^2.17.6",
57
80
  "smol-toml": "^1.6.1",
81
+ "yaml": "^2.9.0",
58
82
  "yjs": "^13.6.0",
59
83
  "zod": "^4.3.6"
60
84
  },
@@ -93,6 +117,7 @@
93
117
  "@types/cross-spawn": "^6.0.6",
94
118
  "@types/react": "^19.0.0",
95
119
  "@types/react-dom": "^19.0.0",
120
+ "@types/sanitize-html": "^2.16.1",
96
121
  "better-sqlite3": "^12.0.0",
97
122
  "clsx": "^2.1.1",
98
123
  "framer-motion": "^12.38.0",
@@ -1,71 +0,0 @@
1
- import { describe, expect, it } from 'vitest'
2
- import {
3
- deriveDocsAccessChange,
4
- detectDocsAclEvent,
5
- isDocsOwner,
6
- resolveDocsAccessRole,
7
- } from '../use-docs-presence-access'
8
-
9
- describe('document access policy', () => {
10
- const document = {
11
- data: {
12
- ownerId: 'owner',
13
- collaborators: JSON.stringify(['editor', 'viewer']),
14
- editors: JSON.stringify(['editor']),
15
- },
16
- }
17
-
18
- it('resolves owner, editor, viewer, and no-access roles from the document ACL', () => {
19
- expect(resolveDocsAccessRole(document, 'owner')).toBe('owner')
20
- expect(resolveDocsAccessRole(document, 'editor')).toBe('editor')
21
- expect(resolveDocsAccessRole(document, 'viewer')).toBe('viewer')
22
- expect(resolveDocsAccessRole(document, 'outsider')).toBe('none')
23
- expect(resolveDocsAccessRole(document, null)).toBe('none')
24
- })
25
-
26
- it('does not expose owner controls while both the document and user are loading', () => {
27
- expect(isDocsOwner(undefined, undefined)).toBe(false)
28
- expect(isDocsOwner(document, undefined)).toBe(false)
29
- expect(isDocsOwner(undefined, 'owner')).toBe(false)
30
- expect(isDocsOwner(document, 'owner')).toBe(true)
31
- })
32
-
33
- it('treats malformed ACL fields as empty lists', () => {
34
- const malformed = {
35
- data: { ownerId: 'owner', collaborators: '{broken', editors: JSON.stringify('editor') },
36
- }
37
- expect(resolveDocsAccessRole(malformed, 'editor')).toBe('none')
38
- })
39
-
40
- it('derives only actionable transitions from the session baseline', () => {
41
- expect(deriveDocsAccessChange('editor', 'viewer', null)).toBe('downgrade')
42
- expect(deriveDocsAccessChange('viewer', 'editor', null)).toBe('upgrade')
43
- expect(deriveDocsAccessChange('viewer', 'none', null)).toBe('revoked')
44
- expect(deriveDocsAccessChange('owner', 'owner', null)).toBeNull()
45
- expect(deriveDocsAccessChange(null, 'viewer', { kind: 'downgrade', at: 20 })).toBe('downgrade')
46
- expect(deriveDocsAccessChange('editor', 'viewer', { kind: 'revoked', at: 20 })).toBe('revoked')
47
- })
48
-
49
- it('accepts only a fresh ACL signal addressed to the current user', () => {
50
- const state = {
51
- aclSignal: {
52
- at: 20,
53
- removed: ['removed-user'],
54
- demoted: ['demoted-user'],
55
- promoted: ['promoted-user'],
56
- },
57
- }
58
-
59
- expect(detectDocsAclEvent(state, 'removed-user', 10)).toEqual({ kind: 'revoked', at: 20 })
60
- expect(detectDocsAclEvent(state, 'demoted-user', 10)).toEqual({
61
- kind: 'downgrade',
62
- at: 20,
63
- })
64
- expect(detectDocsAclEvent(state, 'promoted-user', 10)).toEqual({
65
- kind: 'upgrade',
66
- at: 20,
67
- })
68
- expect(detectDocsAclEvent(state, 'other-user', 10)).toBeNull()
69
- expect(detectDocsAclEvent(state, 'removed-user', 20)).toBeNull()
70
- })
71
- })