studiograph 1.3.3 → 1.3.4

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 (290) hide show
  1. package/dist/agent/orchestrator.d.ts +10 -0
  2. package/dist/agent/orchestrator.js +26 -7
  3. package/dist/agent/orchestrator.js.map +1 -1
  4. package/dist/agent/skills/sync-configuration.md +4 -29
  5. package/dist/agent/skills/sync-setup.md +2 -4
  6. package/dist/agent/tools/graph-tools.d.ts +5 -1
  7. package/dist/agent/tools/graph-tools.js +161 -9
  8. package/dist/agent/tools/graph-tools.js.map +1 -1
  9. package/dist/agent/tools/ops-tools.js +15 -126
  10. package/dist/agent/tools/ops-tools.js.map +1 -1
  11. package/dist/agent/tools/permission-tools.d.ts +15 -14
  12. package/dist/agent/tools/permission-tools.js +65 -128
  13. package/dist/agent/tools/permission-tools.js.map +1 -1
  14. package/dist/agent/tools/sync-tools.d.ts +7 -6
  15. package/dist/agent/tools/sync-tools.js +205 -178
  16. package/dist/agent/tools/sync-tools.js.map +1 -1
  17. package/dist/cli/commands/about.d.ts +13 -0
  18. package/dist/cli/commands/about.js +97 -0
  19. package/dist/cli/commands/about.js.map +1 -0
  20. package/dist/cli/commands/clone.d.ts +5 -2
  21. package/dist/cli/commands/clone.js +131 -62
  22. package/dist/cli/commands/clone.js.map +1 -1
  23. package/dist/cli/commands/connector.d.ts +2 -16
  24. package/dist/cli/commands/connector.js +32 -109
  25. package/dist/cli/commands/connector.js.map +1 -1
  26. package/dist/cli/commands/deploy.d.ts +0 -1
  27. package/dist/cli/commands/deploy.js +13 -103
  28. package/dist/cli/commands/deploy.js.map +1 -1
  29. package/dist/cli/commands/init.js +6 -93
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/join.d.ts +3 -2
  32. package/dist/cli/commands/join.js +142 -97
  33. package/dist/cli/commands/join.js.map +1 -1
  34. package/dist/cli/commands/redeploy.js +1 -2
  35. package/dist/cli/commands/redeploy.js.map +1 -1
  36. package/dist/cli/commands/serve.d.ts +1 -3
  37. package/dist/cli/commands/serve.js +29 -109
  38. package/dist/cli/commands/serve.js.map +1 -1
  39. package/dist/cli/commands/start.js +1 -1
  40. package/dist/cli/commands/start.js.map +1 -1
  41. package/dist/cli/commands/sync-collection.d.ts +14 -0
  42. package/dist/cli/commands/sync-collection.js +366 -0
  43. package/dist/cli/commands/sync-collection.js.map +1 -0
  44. package/dist/cli/commands/sync.d.ts +4 -2
  45. package/dist/cli/commands/sync.js +529 -94
  46. package/dist/cli/commands/sync.js.map +1 -1
  47. package/dist/cli/index.js +15 -30
  48. package/dist/cli/index.js.map +1 -1
  49. package/dist/cli/setup-wizard.d.ts +0 -13
  50. package/dist/cli/setup-wizard.js +6 -81
  51. package/dist/cli/setup-wizard.js.map +1 -1
  52. package/dist/core/graph.d.ts +8 -2
  53. package/dist/core/graph.js +11 -7
  54. package/dist/core/graph.js.map +1 -1
  55. package/dist/core/types.d.ts +149 -21
  56. package/dist/core/types.js +16 -4
  57. package/dist/core/types.js.map +1 -1
  58. package/dist/core/workspace-manager.js +1 -5
  59. package/dist/core/workspace-manager.js.map +1 -1
  60. package/dist/core/workspace.d.ts +11 -4
  61. package/dist/core/workspace.js +61 -26
  62. package/dist/core/workspace.js.map +1 -1
  63. package/dist/integrations/asana.d.ts +26 -0
  64. package/dist/integrations/asana.js +77 -0
  65. package/dist/integrations/asana.js.map +1 -0
  66. package/dist/integrations/figma-local.d.ts +16 -0
  67. package/dist/integrations/figma-local.js +10 -0
  68. package/dist/integrations/figma-local.js.map +1 -0
  69. package/dist/integrations/figma.d.ts +17 -0
  70. package/dist/integrations/figma.js +16 -0
  71. package/dist/integrations/figma.js.map +1 -0
  72. package/dist/integrations/granola.d.ts +24 -0
  73. package/dist/integrations/granola.js +59 -0
  74. package/dist/integrations/granola.js.map +1 -0
  75. package/dist/integrations/linear.d.ts +14 -0
  76. package/dist/integrations/linear.js +70 -0
  77. package/dist/integrations/linear.js.map +1 -0
  78. package/dist/integrations/paper-local.d.ts +14 -0
  79. package/dist/integrations/paper-local.js +10 -0
  80. package/dist/integrations/paper-local.js.map +1 -0
  81. package/dist/integrations/paper.d.ts +2 -0
  82. package/dist/integrations/paper.js +10 -0
  83. package/dist/integrations/paper.js.map +1 -0
  84. package/dist/integrations/pipedrive.d.ts +26 -0
  85. package/dist/integrations/pipedrive.js +97 -0
  86. package/dist/integrations/pipedrive.js.map +1 -0
  87. package/dist/integrations/registry.d.ts +15 -0
  88. package/dist/integrations/registry.js +27 -0
  89. package/dist/integrations/registry.js.map +1 -0
  90. package/dist/integrations/types.d.ts +34 -0
  91. package/dist/integrations/types.js +9 -0
  92. package/dist/integrations/types.js.map +1 -0
  93. package/dist/mcp/connector-manager.d.ts +45 -31
  94. package/dist/mcp/connector-manager.js +164 -116
  95. package/dist/mcp/connector-manager.js.map +1 -1
  96. package/dist/mcp/server-oauth-provider.d.ts +56 -0
  97. package/dist/mcp/server-oauth-provider.js +138 -0
  98. package/dist/mcp/server-oauth-provider.js.map +1 -0
  99. package/dist/server/chrome/chrome.css +142 -28
  100. package/dist/server/chrome/chrome.js +46 -220
  101. package/dist/server/collab-authority.d.ts +70 -0
  102. package/dist/server/collab-authority.js +218 -0
  103. package/dist/server/collab-authority.js.map +1 -0
  104. package/dist/server/collab.d.ts +29 -0
  105. package/dist/server/collab.js +195 -0
  106. package/dist/server/collab.js.map +1 -0
  107. package/dist/server/commit-scheduler.d.ts +39 -0
  108. package/dist/server/commit-scheduler.js +113 -0
  109. package/dist/server/commit-scheduler.js.map +1 -0
  110. package/dist/server/index.d.ts +0 -2
  111. package/dist/server/index.js +166 -55
  112. package/dist/server/index.js.map +1 -1
  113. package/dist/server/routes/auth-api.d.ts +6 -0
  114. package/dist/server/routes/auth-api.js +78 -0
  115. package/dist/server/routes/auth-api.js.map +1 -1
  116. package/dist/server/routes/chat.js +4 -0
  117. package/dist/server/routes/chat.js.map +1 -1
  118. package/dist/server/routes/collab.d.ts +6 -0
  119. package/dist/server/routes/collab.js +10 -0
  120. package/dist/server/routes/collab.js.map +1 -0
  121. package/dist/server/routes/git-http.d.ts +23 -0
  122. package/dist/server/routes/git-http.js +251 -0
  123. package/dist/server/routes/git-http.js.map +1 -0
  124. package/dist/server/routes/graph-api.d.ts +6 -2
  125. package/dist/server/routes/graph-api.js +266 -82
  126. package/dist/server/routes/graph-api.js.map +1 -1
  127. package/dist/server/routes/mcp.d.ts +12 -0
  128. package/dist/server/routes/mcp.js +35 -0
  129. package/dist/server/routes/mcp.js.map +1 -0
  130. package/dist/server/routes/permissions-api.d.ts +6 -4
  131. package/dist/server/routes/permissions-api.js +53 -167
  132. package/dist/server/routes/permissions-api.js.map +1 -1
  133. package/dist/server/routes/sync-api.d.ts +26 -0
  134. package/dist/server/routes/sync-api.js +757 -0
  135. package/dist/server/routes/sync-api.js.map +1 -0
  136. package/dist/server/routes/ws.d.ts +9 -0
  137. package/dist/server/routes/ws.js +131 -0
  138. package/dist/server/routes/ws.js.map +1 -0
  139. package/dist/server/session-manager.d.ts +40 -0
  140. package/dist/server/session-manager.js +132 -0
  141. package/dist/server/session-manager.js.map +1 -0
  142. package/dist/server/ws-hub.d.ts +130 -0
  143. package/dist/server/ws-hub.js +250 -0
  144. package/dist/server/ws-hub.js.map +1 -0
  145. package/dist/server/yjs-manager.d.ts +59 -0
  146. package/dist/server/yjs-manager.js +194 -0
  147. package/dist/server/yjs-manager.js.map +1 -0
  148. package/dist/services/auth-service.d.ts +74 -0
  149. package/dist/services/auth-service.js +286 -6
  150. package/dist/services/auth-service.js.map +1 -1
  151. package/dist/services/git.d.ts +6 -0
  152. package/dist/services/git.js +32 -2
  153. package/dist/services/git.js.map +1 -1
  154. package/dist/services/sync/collection-sync.d.ts +73 -0
  155. package/dist/services/sync/collection-sync.js +726 -0
  156. package/dist/services/sync/collection-sync.js.map +1 -0
  157. package/dist/services/sync/commit.js +5 -20
  158. package/dist/services/sync/commit.js.map +1 -1
  159. package/dist/services/sync/data-fetcher.d.ts +31 -0
  160. package/dist/services/sync/data-fetcher.js +12 -0
  161. package/dist/services/sync/data-fetcher.js.map +1 -0
  162. package/dist/services/sync/entity-refresh.d.ts +30 -0
  163. package/dist/services/sync/entity-refresh.js +275 -0
  164. package/dist/services/sync/entity-refresh.js.map +1 -0
  165. package/dist/services/sync/frontmatter-extractor.d.ts +2 -2
  166. package/dist/services/sync/frontmatter-extractor.js +1 -2
  167. package/dist/services/sync/frontmatter-extractor.js.map +1 -1
  168. package/dist/services/sync/graph-match.js +1 -1
  169. package/dist/services/sync/graph-match.js.map +1 -1
  170. package/dist/services/sync/mcp-client.d.ts +16 -4
  171. package/dist/services/sync/mcp-client.js +34 -20
  172. package/dist/services/sync/mcp-client.js.map +1 -1
  173. package/dist/services/sync/prompts.js +1 -1
  174. package/dist/services/sync/reconciler.js +1 -2
  175. package/dist/services/sync/reconciler.js.map +1 -1
  176. package/dist/services/sync/rest-client.d.ts +40 -0
  177. package/dist/services/sync/rest-client.js +100 -0
  178. package/dist/services/sync/rest-client.js.map +1 -0
  179. package/dist/services/sync/source-config.d.ts +23 -1
  180. package/dist/services/sync/source-config.js +112 -16
  181. package/dist/services/sync/source-config.js.map +1 -1
  182. package/dist/services/sync/source-definitions/asana.d.ts +3 -4
  183. package/dist/services/sync/source-definitions/asana.js +7 -13
  184. package/dist/services/sync/source-definitions/asana.js.map +1 -1
  185. package/dist/services/sync/source-definitions/definitions.d.ts +5 -18
  186. package/dist/services/sync/source-definitions/definitions.js +4 -22
  187. package/dist/services/sync/source-definitions/definitions.js.map +1 -1
  188. package/dist/services/sync/source-definitions/granola.d.ts +1 -1
  189. package/dist/services/sync/source-definitions/granola.js +11 -18
  190. package/dist/services/sync/source-definitions/granola.js.map +1 -1
  191. package/dist/services/sync/source-definitions/linear.d.ts +1 -1
  192. package/dist/services/sync/source-definitions/linear.js +17 -15
  193. package/dist/services/sync/source-definitions/linear.js.map +1 -1
  194. package/dist/services/sync/source-definitions/pipedrive.d.ts +1 -1
  195. package/dist/services/sync/source-definitions/pipedrive.js +6 -15
  196. package/dist/services/sync/source-definitions/pipedrive.js.map +1 -1
  197. package/dist/services/sync/staging.js +1 -2
  198. package/dist/services/sync/staging.js.map +1 -1
  199. package/dist/services/sync/structured-extractor.d.ts +8 -2
  200. package/dist/services/sync/structured-extractor.js +243 -35
  201. package/dist/services/sync/structured-extractor.js.map +1 -1
  202. package/dist/services/sync/types.d.ts +192 -23
  203. package/dist/services/sync/unstructured-extractor.d.ts +1 -3
  204. package/dist/services/sync/unstructured-extractor.js +2 -14
  205. package/dist/services/sync/unstructured-extractor.js.map +1 -1
  206. package/dist/utils/git.d.ts +33 -20
  207. package/dist/utils/git.js +119 -62
  208. package/dist/utils/git.js.map +1 -1
  209. package/dist/utils/preflight.d.ts +1 -15
  210. package/dist/utils/preflight.js +1 -35
  211. package/dist/utils/preflight.js.map +1 -1
  212. package/dist/web/_app/immutable/assets/0.CupILLQs.css +1 -0
  213. package/dist/web/_app/immutable/assets/3.CtJi4Cy9.css +1 -0
  214. package/dist/web/_app/immutable/assets/5.CydFyZSu.css +1 -0
  215. package/dist/web/_app/immutable/assets/6.kqeOo0OW.css +1 -0
  216. package/dist/web/_app/immutable/assets/7.CseIx7qQ.css +1 -0
  217. package/dist/web/_app/immutable/assets/8.BYpFDZHK.css +1 -0
  218. package/dist/web/_app/immutable/assets/AppShell.Ch_ef9hJ.css +1 -0
  219. package/dist/web/_app/immutable/assets/ChatPanel.CP-_8txt.css +1 -0
  220. package/dist/web/_app/immutable/chunks/0oxpWEgM.js +1 -0
  221. package/dist/web/_app/immutable/chunks/B1y7Wy5O.js +18 -0
  222. package/dist/web/_app/immutable/chunks/B7eduG_j.js +64 -0
  223. package/dist/web/_app/immutable/chunks/BBLgaWN8.js +1 -0
  224. package/dist/web/_app/immutable/chunks/BCB5cYCz.js +2 -0
  225. package/dist/web/_app/immutable/chunks/{aosHekRC.js → BPUy9_sS.js} +1 -1
  226. package/dist/web/_app/immutable/chunks/BVBRzmeQ.js +7 -0
  227. package/dist/web/_app/immutable/chunks/{CUzqHQY_.js → BXuvR8Ks.js} +2 -1
  228. package/dist/web/_app/immutable/chunks/BeBar3OL.js +1 -0
  229. package/dist/web/_app/immutable/chunks/BuOTIbJu.js +1 -0
  230. package/dist/web/_app/immutable/chunks/CLFba8FK.js +5 -0
  231. package/dist/web/_app/immutable/chunks/CQCkXCml.js +1 -0
  232. package/dist/web/_app/immutable/chunks/CXuhHL4d.js +1 -0
  233. package/dist/web/_app/immutable/chunks/Cg9NOuOl.js +27 -0
  234. package/dist/web/_app/immutable/chunks/Cs5oz2oJ.js +5 -0
  235. package/dist/web/_app/immutable/chunks/Cs_ROD7H.js +2 -0
  236. package/dist/web/_app/immutable/chunks/D2aTbzFm.js +3 -0
  237. package/dist/web/_app/immutable/chunks/D4FXhiC2.js +1 -0
  238. package/dist/web/_app/immutable/chunks/D4VHRYeB.js +1 -0
  239. package/dist/web/_app/immutable/chunks/DCGSm8Hl.js +1 -0
  240. package/dist/web/_app/immutable/chunks/DP09rP34.js +2 -0
  241. package/dist/web/_app/immutable/chunks/DiP47fAp.js +1 -0
  242. package/dist/web/_app/immutable/chunks/DptGlK8O.js +1 -0
  243. package/dist/web/_app/immutable/chunks/O0fx2ss6.js +1 -0
  244. package/dist/web/_app/immutable/chunks/xBRYfpah.js +1 -0
  245. package/dist/web/_app/immutable/entry/app.FgnywZP_.js +2 -0
  246. package/dist/web/_app/immutable/entry/start.Bsa-zlPf.js +1 -0
  247. package/dist/web/_app/immutable/nodes/0.D3SW-LMc.js +10 -0
  248. package/dist/web/_app/immutable/nodes/1.y0c5TQTP.js +1 -0
  249. package/dist/web/_app/immutable/nodes/2.BQfSep9-.js +1 -0
  250. package/dist/web/_app/immutable/nodes/3.CC4Y-xMM.js +11 -0
  251. package/dist/web/_app/immutable/nodes/{5.BBpmYkAu.js → 4.Dp0Z-oPW.js} +2 -2
  252. package/dist/web/_app/immutable/nodes/5.gjZ03DON.js +2 -0
  253. package/dist/web/_app/immutable/nodes/6.dRNIwcJQ.js +1 -0
  254. package/dist/web/_app/immutable/nodes/7.I4Gjes3o.js +2 -0
  255. package/dist/web/_app/immutable/nodes/8.Dj14D7uH.js +1 -0
  256. package/dist/web/_app/version.json +1 -1
  257. package/dist/web/index.html +10 -12
  258. package/package.json +12 -2
  259. package/dist/web/_app/immutable/assets/0.CDbX4Cwz.css +0 -1
  260. package/dist/web/_app/immutable/assets/3.BJy7pVXi.css +0 -1
  261. package/dist/web/_app/immutable/assets/4.Ad16uh9o.css +0 -1
  262. package/dist/web/_app/immutable/assets/6.Bm2i7O0j.css +0 -1
  263. package/dist/web/_app/immutable/assets/AppShell.D0rmbdqF.css +0 -1
  264. package/dist/web/_app/immutable/assets/ChatPanel.RFD5GGYI.css +0 -1
  265. package/dist/web/_app/immutable/assets/editor.CPAf2SRV.css +0 -1
  266. package/dist/web/_app/immutable/chunks/479TgXB4.js +0 -1
  267. package/dist/web/_app/immutable/chunks/4QY4j-jX.js +0 -1
  268. package/dist/web/_app/immutable/chunks/BFb0g4TQ.js +0 -64
  269. package/dist/web/_app/immutable/chunks/Bopa-Ask.js +0 -1
  270. package/dist/web/_app/immutable/chunks/COwytaCP.js +0 -1
  271. package/dist/web/_app/immutable/chunks/DEJSHbC3.js +0 -1
  272. package/dist/web/_app/immutable/chunks/DNywhIex.js +0 -23
  273. package/dist/web/_app/immutable/chunks/DTUXhwEY.js +0 -1
  274. package/dist/web/_app/immutable/chunks/DThXpa0U.js +0 -6
  275. package/dist/web/_app/immutable/chunks/Dh_H7Owr.js +0 -18
  276. package/dist/web/_app/immutable/chunks/Dml-u95b.js +0 -2
  277. package/dist/web/_app/immutable/chunks/DnlgZ_Tk.js +0 -5
  278. package/dist/web/_app/immutable/chunks/DtVH--hH.js +0 -6
  279. package/dist/web/_app/immutable/chunks/DvKVaE7M.js +0 -1
  280. package/dist/web/_app/immutable/chunks/MbiSz-iW.js +0 -2
  281. package/dist/web/_app/immutable/chunks/bSAC733J.js +0 -1
  282. package/dist/web/_app/immutable/entry/app.BvodXQQ0.js +0 -2
  283. package/dist/web/_app/immutable/entry/start.Bkui3Kyw.js +0 -1
  284. package/dist/web/_app/immutable/nodes/0.DfbCOBhn.js +0 -2
  285. package/dist/web/_app/immutable/nodes/1.vtxUGpe6.js +0 -1
  286. package/dist/web/_app/immutable/nodes/2.Cq29oW4h.js +0 -1
  287. package/dist/web/_app/immutable/nodes/3.SquslPZy.js +0 -1
  288. package/dist/web/_app/immutable/nodes/4.COV8FR8b.js +0 -16
  289. package/dist/web/_app/immutable/nodes/6.BBbh6z9I.js +0 -2
  290. /package/dist/web/_app/immutable/assets/{5.BhKgiXd2.css → 4.BhKgiXd2.css} +0 -0
@@ -0,0 +1,250 @@
1
+ /**
2
+ * WebSocket Hub — event bus for real-time change notifications.
3
+ *
4
+ * Tracks authenticated WebSocket connections and broadcasts entity/repo
5
+ * change events scoped to each client's collection access.
6
+ */
7
+ const PRESENCE_STALE_MS = 90_000; // 90s — heartbeat every 30s, so 3 missed = gone
8
+ /** Assign a stable color to a user based on their id. */
9
+ const PRESENCE_COLORS = ['#4f8ef7', '#e05252', '#52c27d', '#e0a252', '#9b52e0', '#52bde0'];
10
+ function presenceColor(userId) {
11
+ let hash = 0;
12
+ for (let i = 0; i < userId.length; i++)
13
+ hash = (hash * 31 + userId.charCodeAt(i)) | 0;
14
+ return PRESENCE_COLORS[Math.abs(hash) % PRESENCE_COLORS.length];
15
+ }
16
+ export class WsHub {
17
+ clients = new Set();
18
+ heartbeatTimer = null;
19
+ // entityKey → Map<userId, PresenceEntry>
20
+ presenceMap = new Map();
21
+ // userId → { user info, connection count } for online tracking
22
+ onlineMap = new Map();
23
+ constructor() {
24
+ // Ping all clients every 30s; terminate any that haven't responded in 90s.
25
+ // Uses application-level ping/pong (JSON messages) instead of WS ping frames
26
+ // because Railway's proxy answers WS ping frames on behalf of sleeping clients.
27
+ this.heartbeatTimer = setInterval(() => {
28
+ const now = Date.now();
29
+ for (const client of this.clients) {
30
+ if (client.ws.readyState !== 1 /* OPEN */) {
31
+ this.clients.delete(client);
32
+ continue;
33
+ }
34
+ if (now - client.lastPongAt > 90_000) {
35
+ client.ws.terminate();
36
+ this.clients.delete(client);
37
+ continue;
38
+ }
39
+ try {
40
+ client.ws.send(JSON.stringify({ type: 'ping' }));
41
+ }
42
+ catch { /* non-fatal */ }
43
+ }
44
+ // Clean stale presence
45
+ for (const [entityKey, users] of this.presenceMap) {
46
+ for (const [userId, entry] of users) {
47
+ if (now - entry.lastSeen > PRESENCE_STALE_MS) {
48
+ users.delete(userId);
49
+ // No broadcast needed — clients will expire stale entries themselves
50
+ }
51
+ }
52
+ if (users.size === 0)
53
+ this.presenceMap.delete(entityKey);
54
+ }
55
+ }, 30_000);
56
+ }
57
+ addClient(ws, user, collections) {
58
+ const client = {
59
+ ws,
60
+ user,
61
+ collections: collections ? new Set(collections) : null,
62
+ lastPongAt: Date.now(),
63
+ };
64
+ this.clients.add(client);
65
+ // Track online status (ref-counted for multiple tabs)
66
+ if (user) {
67
+ const uid = String(user.id);
68
+ const existing = this.onlineMap.get(uid);
69
+ if (existing) {
70
+ existing.count++;
71
+ }
72
+ else {
73
+ const onlineUser = { id: uid, name: user.displayName };
74
+ this.onlineMap.set(uid, { user: onlineUser, count: 1 });
75
+ this.broadcastOnline('join', onlineUser, ws); // broadcast to others (not new client)
76
+ }
77
+ this.sendOnlineSnapshot(ws); // send current online roster to new client
78
+ }
79
+ ws.on('close', () => {
80
+ this.clients.delete(client);
81
+ if (user) {
82
+ const uid = String(user.id);
83
+ const entry = this.onlineMap.get(uid);
84
+ if (entry) {
85
+ entry.count--;
86
+ if (entry.count === 0) {
87
+ this.onlineMap.delete(uid);
88
+ this.broadcastOnline('leave', entry.user);
89
+ }
90
+ }
91
+ this.clearUserPresence(uid, user.displayName, collections);
92
+ }
93
+ });
94
+ ws.on('error', () => this.clients.delete(client));
95
+ }
96
+ /**
97
+ * Handle a presence message sent by a client.
98
+ * Updates server-side presence state and broadcasts to collection peers.
99
+ */
100
+ handlePresence(action, repo, entityType, entityId, user) {
101
+ const entityKey = `${repo}/${entityType}/${entityId}`;
102
+ const color = user.color ?? presenceColor(user.id);
103
+ if (action === 'leave') {
104
+ const users = this.presenceMap.get(entityKey);
105
+ if (users) {
106
+ users.delete(user.id);
107
+ if (users.size === 0)
108
+ this.presenceMap.delete(entityKey);
109
+ }
110
+ }
111
+ else {
112
+ // enter or heartbeat
113
+ if (!this.presenceMap.has(entityKey)) {
114
+ this.presenceMap.set(entityKey, new Map());
115
+ }
116
+ this.presenceMap.get(entityKey).set(user.id, {
117
+ user: { id: user.id, name: user.name, color },
118
+ lastSeen: Date.now(),
119
+ });
120
+ }
121
+ this.broadcast({
122
+ type: 'presence',
123
+ action,
124
+ repo,
125
+ entityType,
126
+ entityId,
127
+ user: { id: user.id, name: user.name, color },
128
+ timestamp: new Date().toISOString(),
129
+ });
130
+ }
131
+ /** Return current presence for an entity (for initial state on connect). */
132
+ getPresence(repo, entityType, entityId) {
133
+ const entityKey = `${repo}/${entityType}/${entityId}`;
134
+ const users = this.presenceMap.get(entityKey);
135
+ if (!users)
136
+ return [];
137
+ return Array.from(users.values()).map(e => e.user);
138
+ }
139
+ /** Send the full current presence map to a single newly connected client. */
140
+ sendPresenceSnapshot(ws, collections) {
141
+ const now = new Date().toISOString();
142
+ for (const [entityKey, users] of this.presenceMap) {
143
+ const [repo, entityType, entityId] = entityKey.split('/');
144
+ if (!repo || !entityType || !entityId)
145
+ continue;
146
+ if (collections && !collections.has(repo))
147
+ continue;
148
+ for (const entry of users.values()) {
149
+ ws.send(JSON.stringify({
150
+ type: 'presence',
151
+ action: 'enter',
152
+ repo,
153
+ entityType,
154
+ entityId,
155
+ user: entry.user,
156
+ timestamp: now,
157
+ }));
158
+ }
159
+ }
160
+ }
161
+ broadcastOnline(action, user, exclude) {
162
+ const payload = JSON.stringify({ type: 'online', action, user, timestamp: new Date().toISOString() });
163
+ for (const client of this.clients) {
164
+ if (client.ws.readyState !== 1 /* OPEN */ || client.ws === exclude)
165
+ continue;
166
+ client.ws.send(payload);
167
+ }
168
+ }
169
+ sendOnlineSnapshot(ws) {
170
+ const now = new Date().toISOString();
171
+ for (const { user } of this.onlineMap.values()) {
172
+ ws.send(JSON.stringify({ type: 'online', action: 'join', user, timestamp: now }));
173
+ }
174
+ }
175
+ clearUserPresence(userId, userName, collections) {
176
+ for (const [entityKey, users] of this.presenceMap) {
177
+ if (users.has(userId)) {
178
+ users.delete(userId);
179
+ const [repo, entityType, entityId] = entityKey.split('/');
180
+ if (repo && entityType && entityId) {
181
+ this.broadcast({
182
+ type: 'presence', action: 'leave',
183
+ repo, entityType, entityId,
184
+ user: { id: userId, name: userName },
185
+ timestamp: new Date().toISOString(),
186
+ });
187
+ }
188
+ if (users.size === 0)
189
+ this.presenceMap.delete(entityKey);
190
+ }
191
+ }
192
+ }
193
+ broadcast(event) {
194
+ const payload = JSON.stringify(event);
195
+ for (const client of this.clients) {
196
+ if (client.ws.readyState !== 1 /* OPEN */)
197
+ continue;
198
+ if (!this.canReceive(client, event.repo))
199
+ continue;
200
+ client.ws.send(payload);
201
+ }
202
+ }
203
+ /** Send an event directly to all connections belonging to a specific user, bypassing repo filtering. */
204
+ broadcastToUser(userId, event) {
205
+ const payload = JSON.stringify(event);
206
+ for (const client of this.clients) {
207
+ if (client.ws.readyState !== 1 /* OPEN */)
208
+ continue;
209
+ if (!client.user || String(client.user.id) !== userId)
210
+ continue;
211
+ client.ws.send(payload);
212
+ }
213
+ }
214
+ /** Record an app-level pong from a client (called by ws route on message). */
215
+ handlePong(ws) {
216
+ for (const client of this.clients) {
217
+ if (client.ws === ws) {
218
+ client.lastPongAt = Date.now();
219
+ break;
220
+ }
221
+ }
222
+ }
223
+ /** Iterate over all connected client sockets (for collab broadcasting). */
224
+ forEachSocket(fn) {
225
+ for (const client of this.clients) {
226
+ if (client.ws.readyState === 1 /* OPEN */)
227
+ fn(client.ws);
228
+ }
229
+ }
230
+ get connectionCount() {
231
+ return this.clients.size;
232
+ }
233
+ destroy() {
234
+ if (this.heartbeatTimer) {
235
+ clearInterval(this.heartbeatTimer);
236
+ this.heartbeatTimer = null;
237
+ }
238
+ for (const client of this.clients) {
239
+ client.ws.close();
240
+ }
241
+ this.clients.clear();
242
+ }
243
+ canReceive(client, repo) {
244
+ // Open mode or null collections = receives everything
245
+ if (!client.collections)
246
+ return true;
247
+ return client.collections.has(repo);
248
+ }
249
+ }
250
+ //# sourceMappingURL=ws-hub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ws-hub.js","sourceRoot":"","sources":["../../src/server/ws-hub.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAiHH,MAAM,iBAAiB,GAAG,MAAM,CAAC,CAAC,gDAAgD;AAElF,yDAAyD;AACzD,MAAM,eAAe,GAAG,CAAC,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;AAC3F,SAAS,aAAa,CAAC,MAAc;IACnC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,GAAG,CAAC,IAAI,GAAG,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;IACtF,OAAO,eAAe,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,OAAO,KAAK;IACR,OAAO,GAAG,IAAI,GAAG,EAAY,CAAC;IAC9B,cAAc,GAA0C,IAAI,CAAC;IACrE,yCAAyC;IACjC,WAAW,GAAG,IAAI,GAAG,EAAsC,CAAC;IACpE,+DAA+D;IACvD,SAAS,GAAG,IAAI,GAAG,EAAiE,CAAC;IAE7F;QACE,2EAA2E;QAC3E,6EAA6E;QAC7E,gFAAgF;QAChF,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE;YACrC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;YACvB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBAClC,IAAI,MAAM,CAAC,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU,EAAE,CAAC;oBAC1C,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;oBAC5B,SAAS;gBACX,CAAC;gBACD,IAAI,GAAG,GAAG,MAAM,CAAC,UAAU,GAAG,MAAM,EAAE,CAAC;oBACrC,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC;oBACtB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;oBAC5B,SAAS;gBACX,CAAC;gBACD,IAAI,CAAC;oBAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC;gBAAC,CAAC;gBAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC;YACrF,CAAC;YACD,uBAAuB;YACvB,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;gBAClD,KAAK,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,KAAK,EAAE,CAAC;oBACpC,IAAI,GAAG,GAAG,KAAK,CAAC,QAAQ,GAAG,iBAAiB,EAAE,CAAC;wBAC7C,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;wBACrB,qEAAqE;oBACvE,CAAC;gBACH,CAAC;gBACD,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;oBAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC,EAAE,MAAM,CAAC,CAAC;IACb,CAAC;IAED,SAAS,CAAC,EAAa,EAAE,IAAqB,EAAE,WAA4B;QAC1E,MAAM,MAAM,GAAa;YACvB,EAAE;YACF,IAAI;YACJ,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI;YACtD,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE;SACvB,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAEzB,sDAAsD;QACtD,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC5B,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YACzC,IAAI,QAAQ,EAAE,CAAC;gBACb,QAAQ,CAAC,KAAK,EAAE,CAAC;YACnB,CAAC;iBAAM,CAAC;gBACN,MAAM,UAAU,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;gBACvD,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;gBACxD,IAAI,CAAC,eAAe,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC,CAAC,uCAAuC;YACvF,CAAC;YACD,IAAI,CAAC,kBAAkB,CAAC,EAAE,CAAC,CAAC,CAAC,2CAA2C;QAC1E,CAAC;QAED,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAClB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,IAAI,EAAE,CAAC;gBACT,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBACtC,IAAI,KAAK,EAAE,CAAC;oBACV,KAAK,CAAC,KAAK,EAAE,CAAC;oBACd,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC,EAAE,CAAC;wBACtB,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;wBAC3B,IAAI,CAAC,eAAe,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;oBAC5C,CAAC;gBACH,CAAC;gBACD,IAAI,CAAC,iBAAiB,CAAC,GAAG,EAAE,IAAI,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC,CAAC,CAAC;QACH,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACpD,CAAC;IAED;;;OAGG;IACH,cAAc,CACZ,MAAuC,EACvC,IAAY,EACZ,UAAkB,EAClB,QAAgB,EAChB,IAAkD;QAElD,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,UAAU,IAAI,QAAQ,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEnD,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;YACvB,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;YAC9C,IAAI,KAAK,EAAE,CAAC;gBACV,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;gBACtB,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;oBAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;aAAM,CAAC;YACN,qBAAqB;YACrB,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;gBACrC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,GAAG,EAAE,CAAC,CAAC;YAC7C,CAAC;YACD,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAE,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE;gBAC5C,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;gBAC7C,QAAQ,EAAE,IAAI,CAAC,GAAG,EAAE;aACrB,CAAC,CAAC;QACL,CAAC;QAED,IAAI,CAAC,SAAS,CAAC;YACb,IAAI,EAAE,UAAU;YAChB,MAAM;YACN,IAAI;YACJ,UAAU;YACV,QAAQ;YACR,IAAI,EAAE,EAAE,EAAE,EAAE,IAAI,CAAC,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE;YAC7C,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACpC,CAAC,CAAC;IACL,CAAC;IAED,4EAA4E;IAC5E,WAAW,CAAC,IAAY,EAAE,UAAkB,EAAE,QAAgB;QAC5D,MAAM,SAAS,GAAG,GAAG,IAAI,IAAI,UAAU,IAAI,QAAQ,EAAE,CAAC;QACtD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC;QAC9C,IAAI,CAAC,KAAK;YAAE,OAAO,EAAE,CAAC;QACtB,OAAO,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;IACrD,CAAC;IAED,6EAA6E;IAC7E,oBAAoB,CAAC,EAAa,EAAE,WAA+B;QACjE,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrC,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAClD,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YAC1D,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,IAAI,CAAC,QAAQ;gBAAE,SAAS;YAChD,IAAI,WAAW,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,SAAS;YACpD,KAAK,MAAM,KAAK,IAAI,KAAK,CAAC,MAAM,EAAE,EAAE,CAAC;gBACnC,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC;oBACrB,IAAI,EAAE,UAAU;oBAChB,MAAM,EAAE,OAAO;oBACf,IAAI;oBACJ,UAAU;oBACV,QAAQ;oBACR,IAAI,EAAE,KAAK,CAAC,IAAI;oBAChB,SAAS,EAAE,GAAG;iBACf,CAAC,CAAC,CAAC;YACN,CAAC;QACH,CAAC;IACH,CAAC;IAEO,eAAe,CAAC,MAAwB,EAAE,IAAkC,EAAE,OAAmB;QACvG,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC;QACtG,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,MAAM,CAAC,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU,IAAI,MAAM,CAAC,EAAE,KAAK,OAAO;gBAAE,SAAS;YAC7E,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAEO,kBAAkB,CAAC,EAAa;QACtC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACrC,KAAK,MAAM,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC;YAC/C,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,CAAC,CAAC;QACpF,CAAC;IACH,CAAC;IAEO,iBAAiB,CAAC,MAAc,EAAE,QAAgB,EAAE,WAA4B;QACtF,KAAK,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YAClD,IAAI,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC;gBACtB,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBACrB,MAAM,CAAC,IAAI,EAAE,UAAU,EAAE,QAAQ,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;gBAC1D,IAAI,IAAI,IAAI,UAAU,IAAI,QAAQ,EAAE,CAAC;oBACnC,IAAI,CAAC,SAAS,CAAC;wBACb,IAAI,EAAE,UAAU,EAAE,MAAM,EAAE,OAAO;wBACjC,IAAI,EAAE,UAAU,EAAE,QAAQ;wBAC1B,IAAI,EAAE,EAAE,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE;wBACpC,SAAS,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;qBACpC,CAAC,CAAC;gBACL,CAAC;gBACD,IAAI,KAAK,CAAC,IAAI,KAAK,CAAC;oBAAE,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YAC3D,CAAC;QACH,CAAC;IACH,CAAC;IAED,SAAS,CAAC,KAAc;QACtB,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACtC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,MAAM,CAAC,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU;gBAAE,SAAS;YACpD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAG,KAAa,CAAC,IAAI,CAAC;gBAAE,SAAS;YAC5D,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,wGAAwG;IACxG,eAAe,CAAC,MAAc,EAAE,KAAc;QAC5C,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACtC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,MAAM,CAAC,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU;gBAAE,SAAS;YACpD,IAAI,CAAC,MAAM,CAAC,IAAI,IAAI,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,MAAM;gBAAE,SAAS;YAChE,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,8EAA8E;IAC9E,UAAU,CAAC,EAAa;QACtB,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,MAAM,CAAC,EAAE,KAAK,EAAE,EAAE,CAAC;gBACrB,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;gBAC/B,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,2EAA2E;IAC3E,aAAa,CAAC,EAA2B;QACvC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,MAAM,CAAC,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU;gBAAE,EAAE,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC3D,CAAC;IACH,CAAC;IAED,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACnC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAEO,UAAU,CAAC,MAAgB,EAAE,IAAY;QAC/C,sDAAsD;QACtD,IAAI,CAAC,MAAM,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC;QACrC,OAAO,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;CACF"}
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Server-side Y.Doc manager for collaborative editing.
3
+ *
4
+ * Maintains one Y.Doc per actively edited entity. The server is the single
5
+ * seeding authority — it loads entity content from disk into the Y.Doc when
6
+ * the first client connects. Clients start with empty Y.Docs and receive
7
+ * content via the Yjs sync protocol. This prevents the double-seeding bug
8
+ * where two clients independently insert the same content.
9
+ *
10
+ * Lifecycle:
11
+ * 1. First client opens entity → server creates Y.Doc, seeds from disk
12
+ * 2. Server sends sync-step-1 to client → client responds → both converge
13
+ * 3. Client edits → binary update to server → server applies + relays to peers
14
+ * 4. Last client leaves → server destroys Y.Doc (frees memory)
15
+ */
16
+ import type { WorkspaceManager } from '../core/workspace-manager.js';
17
+ export declare class YjsManager {
18
+ private workspaceManager;
19
+ private docs;
20
+ constructor(workspaceManager: WorkspaceManager);
21
+ /**
22
+ * Parse a docName ("repo/entityType/entityId") into its components.
23
+ */
24
+ private parseDocName;
25
+ /**
26
+ * Get or create a Y.Doc for an entity. Seeds from disk on first access.
27
+ */
28
+ private getOrCreate;
29
+ /**
30
+ * Add a client to a document. Sends initial sync state to the client.
31
+ */
32
+ addClient(docName: string, ws: import('ws').WebSocket): void;
33
+ /**
34
+ * Remove a client from a document. Destroys Y.Doc when last client leaves.
35
+ */
36
+ removeClient(docName: string, ws: import('ws').WebSocket): void;
37
+ /**
38
+ * Remove a client from ALL documents (called on WS close).
39
+ */
40
+ removeClientFromAll(ws: import('ws').WebSocket): void;
41
+ /**
42
+ * Handle a binary Yjs message from a client.
43
+ * Applies sync messages to the server Y.Doc and relays to peers.
44
+ * Awareness messages are relayed without server processing.
45
+ */
46
+ handleMessage(docName: string, sender: import('ws').WebSocket, msgType: number, payload: Uint8Array): void;
47
+ /**
48
+ * Encode and send a framed binary message to a client.
49
+ */
50
+ private sendToClient;
51
+ /**
52
+ * Get current Y.Doc text content for a document (for debugging/autosave).
53
+ */
54
+ getText(docName: string): string | null;
55
+ /**
56
+ * Destroy all Y.Docs (server shutdown).
57
+ */
58
+ destroy(): void;
59
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * Server-side Y.Doc manager for collaborative editing.
3
+ *
4
+ * Maintains one Y.Doc per actively edited entity. The server is the single
5
+ * seeding authority — it loads entity content from disk into the Y.Doc when
6
+ * the first client connects. Clients start with empty Y.Docs and receive
7
+ * content via the Yjs sync protocol. This prevents the double-seeding bug
8
+ * where two clients independently insert the same content.
9
+ *
10
+ * Lifecycle:
11
+ * 1. First client opens entity → server creates Y.Doc, seeds from disk
12
+ * 2. Server sends sync-step-1 to client → client responds → both converge
13
+ * 3. Client edits → binary update to server → server applies + relays to peers
14
+ * 4. Last client leaves → server destroys Y.Doc (frees memory)
15
+ */
16
+ import * as Y from 'yjs';
17
+ import * as syncProtocol from 'y-protocols/sync.js';
18
+ import * as awarenessProtocol from 'y-protocols/awareness.js';
19
+ import * as encoding from 'lib0/encoding';
20
+ import * as decoding from 'lib0/decoding';
21
+ const MSG_SYNC = 0;
22
+ const MSG_AWARENESS = 1;
23
+ export class YjsManager {
24
+ workspaceManager;
25
+ docs = new Map();
26
+ constructor(workspaceManager) {
27
+ this.workspaceManager = workspaceManager;
28
+ }
29
+ /**
30
+ * Parse a docName ("repo/entityType/entityId") into its components.
31
+ */
32
+ parseDocName(docName) {
33
+ const parts = docName.split('/');
34
+ if (parts.length < 3)
35
+ return null;
36
+ return { repo: parts[0], entityType: parts[1], entityId: parts.slice(2).join('/') };
37
+ }
38
+ /**
39
+ * Get or create a Y.Doc for an entity. Seeds from disk on first access.
40
+ */
41
+ getOrCreate(docName) {
42
+ const existing = this.docs.get(docName);
43
+ if (existing)
44
+ return existing;
45
+ // Load entity content from disk
46
+ const parsed = this.parseDocName(docName);
47
+ if (!parsed)
48
+ return null;
49
+ const graph = this.workspaceManager.getGraph(parsed.repo);
50
+ if (!graph)
51
+ return null;
52
+ let diskContent = '';
53
+ try {
54
+ const entity = graph.get(parsed.entityType, parsed.entityId);
55
+ diskContent = entity?.document?.content ?? '';
56
+ }
57
+ catch {
58
+ // Entity not found — start with empty doc
59
+ }
60
+ const ydoc = new Y.Doc();
61
+ const ytext = ydoc.getText('content');
62
+ const awareness = new awarenessProtocol.Awareness(ydoc);
63
+ // Seed Y.Doc with disk content (server is the single seeding authority)
64
+ if (diskContent.length > 0) {
65
+ ydoc.transact(() => {
66
+ ytext.insert(0, diskContent);
67
+ }, 'server-seed');
68
+ }
69
+ const entry = { ydoc, awareness, clients: new Set() };
70
+ this.docs.set(docName, entry);
71
+ return entry;
72
+ }
73
+ /**
74
+ * Add a client to a document. Sends initial sync state to the client.
75
+ */
76
+ addClient(docName, ws) {
77
+ const entry = this.getOrCreate(docName);
78
+ if (!entry)
79
+ return;
80
+ entry.clients.add(ws);
81
+ // Send sync-step-1 to the new client (our state vector)
82
+ // so the client can respond with what we're missing (sync-step-2)
83
+ const encoder = encoding.createEncoder();
84
+ syncProtocol.writeSyncStep1(encoder, entry.ydoc);
85
+ this.sendToClient(ws, docName, MSG_SYNC, encoding.toUint8Array(encoder));
86
+ // Also send sync-step-2 immediately (our full state)
87
+ // so the client gets our content without a round trip
88
+ const encoder2 = encoding.createEncoder();
89
+ syncProtocol.writeSyncStep2(encoder2, entry.ydoc);
90
+ this.sendToClient(ws, docName, MSG_SYNC, encoding.toUint8Array(encoder2));
91
+ // Send current awareness states to the new client
92
+ const awarenessStates = entry.awareness.getStates();
93
+ if (awarenessStates.size > 0) {
94
+ const clients = Array.from(awarenessStates.keys());
95
+ const update = awarenessProtocol.encodeAwarenessUpdate(entry.awareness, clients);
96
+ this.sendToClient(ws, docName, MSG_AWARENESS, update);
97
+ }
98
+ }
99
+ /**
100
+ * Remove a client from a document. Destroys Y.Doc when last client leaves.
101
+ */
102
+ removeClient(docName, ws) {
103
+ const entry = this.docs.get(docName);
104
+ if (!entry)
105
+ return;
106
+ entry.clients.delete(ws);
107
+ if (entry.clients.size === 0) {
108
+ entry.awareness.destroy();
109
+ entry.ydoc.destroy();
110
+ this.docs.delete(docName);
111
+ }
112
+ }
113
+ /**
114
+ * Remove a client from ALL documents (called on WS close).
115
+ */
116
+ removeClientFromAll(ws) {
117
+ for (const [docName, entry] of this.docs) {
118
+ if (entry.clients.has(ws)) {
119
+ this.removeClient(docName, ws);
120
+ }
121
+ }
122
+ }
123
+ /**
124
+ * Handle a binary Yjs message from a client.
125
+ * Applies sync messages to the server Y.Doc and relays to peers.
126
+ * Awareness messages are relayed without server processing.
127
+ */
128
+ handleMessage(docName, sender, msgType, payload) {
129
+ const entry = this.docs.get(docName);
130
+ if (!entry)
131
+ return;
132
+ if (msgType === MSG_SYNC) {
133
+ // Apply sync message to server Y.Doc
134
+ const decoder = decoding.createDecoder(payload);
135
+ const encoder = encoding.createEncoder();
136
+ syncProtocol.readSyncMessage(decoder, encoder, entry.ydoc, 'ws-client');
137
+ // If the sync produced a response, send it back to the sender
138
+ if (encoding.length(encoder) > 0) {
139
+ this.sendToClient(sender, docName, MSG_SYNC, encoding.toUint8Array(encoder));
140
+ }
141
+ // Relay the original message to all OTHER clients
142
+ for (const client of entry.clients) {
143
+ if (client === sender || client.readyState !== 1)
144
+ continue;
145
+ this.sendToClient(client, docName, MSG_SYNC, payload);
146
+ }
147
+ }
148
+ else if (msgType === MSG_AWARENESS) {
149
+ // Apply awareness to server's awareness instance
150
+ awarenessProtocol.applyAwarenessUpdate(entry.awareness, payload, sender);
151
+ // Relay to all OTHER clients
152
+ for (const client of entry.clients) {
153
+ if (client === sender || client.readyState !== 1)
154
+ continue;
155
+ this.sendToClient(client, docName, MSG_AWARENESS, payload);
156
+ }
157
+ }
158
+ }
159
+ /**
160
+ * Encode and send a framed binary message to a client.
161
+ */
162
+ sendToClient(ws, docName, msgType, payload) {
163
+ const nameBytes = Buffer.from(docName, 'utf8');
164
+ const frame = Buffer.alloc(2 + nameBytes.length + 1 + payload.length);
165
+ frame.writeUInt16BE(nameBytes.length, 0);
166
+ nameBytes.copy(frame, 2);
167
+ frame[2 + nameBytes.length] = msgType;
168
+ Buffer.from(payload).copy(frame, 2 + nameBytes.length + 1);
169
+ try {
170
+ ws.send(frame);
171
+ }
172
+ catch { /* non-fatal */ }
173
+ }
174
+ /**
175
+ * Get current Y.Doc text content for a document (for debugging/autosave).
176
+ */
177
+ getText(docName) {
178
+ const entry = this.docs.get(docName);
179
+ if (!entry)
180
+ return null;
181
+ return entry.ydoc.getText('content').toString();
182
+ }
183
+ /**
184
+ * Destroy all Y.Docs (server shutdown).
185
+ */
186
+ destroy() {
187
+ for (const [, entry] of this.docs) {
188
+ entry.awareness.destroy();
189
+ entry.ydoc.destroy();
190
+ }
191
+ this.docs.clear();
192
+ }
193
+ }
194
+ //# sourceMappingURL=yjs-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"yjs-manager.js","sourceRoot":"","sources":["../../src/server/yjs-manager.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,CAAC,MAAM,KAAK,CAAC;AACzB,OAAO,KAAK,YAAY,MAAM,qBAAqB,CAAC;AACpD,OAAO,KAAK,iBAAiB,MAAM,0BAA0B,CAAC;AAC9D,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAC1C,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAI1C,MAAM,QAAQ,GAAG,CAAC,CAAC;AACnB,MAAM,aAAa,GAAG,CAAC,CAAC;AAQxB,MAAM,OAAO,UAAU;IAGD;IAFZ,IAAI,GAAG,IAAI,GAAG,EAAoB,CAAC;IAE3C,YAAoB,gBAAkC;QAAlC,qBAAgB,GAAhB,gBAAgB,CAAkB;IAAG,CAAC;IAE1D;;OAEG;IACK,YAAY,CAAC,OAAe;QAClC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACjC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QAClC,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,KAAK,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACtF,CAAC;IAED;;OAEG;IACK,WAAW,CAAC,OAAe;QACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,QAAQ;YAAE,OAAO,QAAQ,CAAC;QAE9B,gCAAgC;QAChC,MAAM,MAAM,GAAG,IAAI,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAEzB,MAAM,KAAK,GAAG,IAAI,CAAC,gBAAgB,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;QAC1D,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QAExB,IAAI,WAAW,GAAG,EAAE,CAAC;QACrB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,UAAwB,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC;YAC3E,WAAW,GAAG,MAAM,EAAE,QAAQ,EAAE,OAAO,IAAI,EAAE,CAAC;QAChD,CAAC;QAAC,MAAM,CAAC;YACP,0CAA0C;QAC5C,CAAC;QAED,MAAM,IAAI,GAAG,IAAI,CAAC,CAAC,GAAG,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QACtC,MAAM,SAAS,GAAG,IAAI,iBAAiB,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC;QAExD,wEAAwE;QACxE,IAAI,WAAW,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE;gBACjB,KAAK,CAAC,MAAM,CAAC,CAAC,EAAE,WAAW,CAAC,CAAC;YAC/B,CAAC,EAAE,aAAa,CAAC,CAAC;QACpB,CAAC;QAED,MAAM,KAAK,GAAa,EAAE,IAAI,EAAE,SAAS,EAAE,OAAO,EAAE,IAAI,GAAG,EAAE,EAAE,CAAC;QAChE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;QAC9B,OAAO,KAAK,CAAC;IACf,CAAC;IAED;;OAEG;IACH,SAAS,CAAC,OAAe,EAAE,EAA0B;QACnD,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;QACxC,IAAI,CAAC,KAAK;YAAE,OAAO;QAEnB,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAEtB,wDAAwD;QACxD,kEAAkE;QAClE,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,EAAE,CAAC;QACzC,YAAY,CAAC,cAAc,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QACjD,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;QAEzE,qDAAqD;QACrD,sDAAsD;QACtD,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,EAAE,CAAC;QAC1C,YAAY,CAAC,cAAc,CAAC,QAAQ,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;QAClD,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC;QAE1E,kDAAkD;QAClD,MAAM,eAAe,GAAG,KAAK,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC;QACpD,IAAI,eAAe,CAAC,IAAI,GAAG,CAAC,EAAE,CAAC;YAC7B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,eAAe,CAAC,IAAI,EAAE,CAAa,CAAC;YAC/D,MAAM,MAAM,GAAG,iBAAiB,CAAC,qBAAqB,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;YACjF,IAAI,CAAC,YAAY,CAAC,EAAE,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,CAAC,CAAC;QACxD,CAAC;IACH,CAAC;IAED;;OAEG;IACH,YAAY,CAAC,OAAe,EAAE,EAA0B;QACtD,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK;YAAE,OAAO;QAEnB,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAEzB,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YAC7B,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YACrB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;QAC5B,CAAC;IACH,CAAC;IAED;;OAEG;IACH,mBAAmB,CAAC,EAA0B;QAC5C,KAAK,MAAM,CAAC,OAAO,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YACzC,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;gBAC1B,IAAI,CAAC,YAAY,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;YACjC,CAAC;QACH,CAAC;IACH,CAAC;IAED;;;;OAIG;IACH,aAAa,CAAC,OAAe,EAAE,MAA8B,EAAE,OAAe,EAAE,OAAmB;QACjG,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK;YAAE,OAAO;QAEnB,IAAI,OAAO,KAAK,QAAQ,EAAE,CAAC;YACzB,qCAAqC;YACrC,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC;YAChD,MAAM,OAAO,GAAG,QAAQ,CAAC,aAAa,EAAE,CAAC;YACzC,YAAY,CAAC,eAAe,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;YAExE,8DAA8D;YAC9D,IAAI,QAAQ,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC;gBACjC,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC;YAC/E,CAAC;YAED,kDAAkD;YAClD,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBACnC,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,CAAC,UAAU,KAAK,CAAC;oBAAE,SAAS;gBAC3D,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;YACxD,CAAC;QACH,CAAC;aAAM,IAAI,OAAO,KAAK,aAAa,EAAE,CAAC;YACrC,iDAAiD;YACjD,iBAAiB,CAAC,oBAAoB,CAAC,KAAK,CAAC,SAAS,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;YAEzE,6BAA6B;YAC7B,KAAK,MAAM,MAAM,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;gBACnC,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM,CAAC,UAAU,KAAK,CAAC;oBAAE,SAAS;gBAC3D,IAAI,CAAC,YAAY,CAAC,MAAM,EAAE,OAAO,EAAE,aAAa,EAAE,OAAO,CAAC,CAAC;YAC7D,CAAC;QACH,CAAC;IACH,CAAC;IAED;;OAEG;IACK,YAAY,CAAC,EAA0B,EAAE,OAAe,EAAE,OAAe,EAAE,OAAmB;QACpG,MAAM,SAAS,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAC/C,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;QACtE,KAAK,CAAC,aAAa,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;QACzC,SAAS,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QACzB,KAAK,CAAC,CAAC,GAAG,SAAS,CAAC,MAAM,CAAC,GAAG,OAAO,CAAC;QACtC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;QAC3D,IAAI,CAAC;YAAC,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAAC,CAAC;QAAC,MAAM,CAAC,CAAC,eAAe,CAAC,CAAC;IACnD,CAAC;IAED;;OAEG;IACH,OAAO,CAAC,OAAe;QACrB,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC;QACxB,OAAO,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,QAAQ,EAAE,CAAC;IAClD,CAAC;IAED;;OAEG;IACH,OAAO;QACL,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC;YAClC,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;YAC1B,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;QACvB,CAAC;QACD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC;IACpB,CAAC;CACF"}
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * SQLite-backed user store with bcrypt password hashing and JWT sessions.
5
5
  * Database lives at .studiograph/auth.db inside the workspace.
6
+ *
7
+ * Auth state is portable via .studiograph/auth-seed.json — a JSON export of
8
+ * all users (with bcrypt hashes) and the JWT secret. The seed file is
9
+ * committed to the config repo so it syncs between local and Railway:
10
+ * local setup → commit → push → Railway redeploy → users restored from seed
6
11
  */
7
12
  export interface AuthUser {
8
13
  id: number;
@@ -11,9 +16,26 @@ export interface AuthUser {
11
16
  role: 'admin' | 'member';
12
17
  createdAt: string;
13
18
  }
19
+ export interface AuthSeed {
20
+ jwtSecret: string;
21
+ apiKey?: string;
22
+ users: Array<{
23
+ email: string;
24
+ password_hash: string;
25
+ display_name: string;
26
+ role: string;
27
+ created_at: string;
28
+ }>;
29
+ collection_access?: Array<{
30
+ user_email: string;
31
+ collection_name: string;
32
+ }>;
33
+ }
14
34
  export declare class AuthService {
15
35
  private db;
16
36
  private jwtSecret;
37
+ private apiKey;
38
+ private sgDir;
17
39
  constructor(workspacePath: string);
18
40
  createUser(email: string, password: string, displayName: string, role?: 'admin' | 'member'): AuthUser;
19
41
  authenticate(email: string, password: string): {
@@ -24,8 +46,60 @@ export declare class AuthService {
24
46
  listUsers(): AuthUser[];
25
47
  deleteUser(email: string): boolean;
26
48
  updatePassword(email: string, newPassword: string): boolean;
49
+ grantCollectionAccess(userId: number, collectionName: string): void;
50
+ revokeCollectionAccess(userId: number, collectionName: string): void;
51
+ getUserCollections(userId: number): string[];
52
+ getCollectionUsers(collectionName: string): AuthUser[];
53
+ renameCollection(oldName: string, newName: string): void;
54
+ /** Get stored OAuth data (tokens, clientInfo, codeVerifier) for a user+connector. */
55
+ getOAuthData(userId: number, connectorName: string): Record<string, any> | null;
56
+ /** Save OAuth data (upsert). */
57
+ saveOAuthData(userId: number, connectorName: string, data: Record<string, any>): void;
58
+ /** Clear OAuth data for a user+connector. */
59
+ clearOAuthData(userId: number, connectorName: string): void;
60
+ /** Check if a user has OAuth tokens for a connector. */
61
+ hasOAuthTokens(userId: number, connectorName: string): boolean;
27
62
  hasUsers(): boolean;
28
63
  getUserCount(): number;
64
+ /** Get the workspace API key (for MCP and programmatic access). */
65
+ getApiKey(): string;
66
+ /** Check whether the given string matches the workspace API key. */
67
+ validateApiKey(key: string): boolean;
68
+ /**
69
+ * Ensure the current API key is present in auth-seed.json and pushed to the
70
+ * config repo. Called on server startup so the key is always reachable by CLI.
71
+ * Non-fatal — never throws.
72
+ */
73
+ syncApiKeyToSeed(): void;
74
+ /** Regenerate the workspace API key. Returns the new key. */
75
+ regenerateApiKey(): string;
76
+ /**
77
+ * Ensure auth.db and auth-secret are in .studiograph/.gitignore.
78
+ * Only the seed JSON file should be committed to the config repo.
79
+ */
80
+ private ensureGitignore;
81
+ /**
82
+ * Build and return the current auth seed data (users + JWT secret + collection access).
83
+ * Does not write to disk — use exportSeed() for that.
84
+ */
85
+ getSeedData(): AuthSeed;
86
+ /**
87
+ * Export all users and JWT secret to .studiograph/auth-seed.json.
88
+ * If .studiograph/ is a git repo with a remote, auto-commits and pushes
89
+ * the seed file so it syncs between environments.
90
+ */
91
+ exportSeed(): void;
92
+ /**
93
+ * Commit and push auth-seed.json if .studiograph/ is a git repo with a remote.
94
+ * Non-fatal — logs warnings on failure but never throws.
95
+ */
96
+ private pushSeed;
97
+ /**
98
+ * Import users and JWT secret from .studiograph/auth-seed.json.
99
+ * Uses INSERT OR IGNORE so it's safe to call on non-empty databases
100
+ * (e.g. after `studiograph pull` brings a new seed file).
101
+ */
102
+ applySeed(): void;
29
103
  close(): void;
30
104
  private toAuthUser;
31
105
  }