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,757 @@
1
+ /**
2
+ * Sync API routes
3
+ *
4
+ * REST endpoints for collection-scoped sync:
5
+ * POST /api/repos/:repo/sync — run sync for a collection
6
+ * POST /api/repos/:repo/entities/:type/:id/refresh — get refresh diff
7
+ * POST /api/repos/:repo/entities/:type/:id/refresh/apply — apply refresh
8
+ * GET /api/repos/:repo/sync/status — sync config and last sync time
9
+ * GET /api/sources — list source definitions
10
+ * GET /api/sources/:name — get a single source definition
11
+ * POST /api/sources/:name — create/update source definition
12
+ * DELETE /api/sources/:name — remove source definition
13
+ * GET /api/connectors — list available MCP connectors
14
+ * GET /api/connectors/:name/config — get connector config (secrets masked)
15
+ * POST /api/connectors/:name/config — create/update connector config
16
+ * DELETE /api/connectors/:name/config — remove connector config
17
+ * POST /api/connectors/:name/oauth/authorize — initiate OAuth flow
18
+ * GET /api/oauth/callback/:name — OAuth callback (browser redirect)
19
+ * GET /api/connectors/:name/oauth/status — poll OAuth connection status
20
+ * POST /api/connectors/:name/oauth/disconnect — clear OAuth tokens
21
+ */
22
+ import { Workspace } from '../../core/workspace.js';
23
+ import { syncRepo, syncWorkspaceRule } from '../../services/sync/collection-sync.js';
24
+ import { refreshEntity } from '../../services/sync/entity-refresh.js';
25
+ import { SourceConfigManager } from '../../services/sync/source-config.js';
26
+ import { ConnectorManager, KNOWN_CONNECTORS } from '../../mcp/connector-manager.js';
27
+ import { ServerOAuthProvider, decodeOAuthState } from '../../mcp/server-oauth-provider.js';
28
+ import { auth as mcpAuth } from '@modelcontextprotocol/sdk/client/auth.js';
29
+ /**
30
+ * Creates a fetch wrapper for OAuth that handles providers with broken or missing
31
+ * metadata discovery endpoints. When custom OAuth endpoints are configured, returns
32
+ * synthetic metadata so the SDK uses the correct authorize/token URLs.
33
+ */
34
+ function createOAuthFetch(oauthApp) {
35
+ return async (input, init) => {
36
+ const url = typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url;
37
+ // Intercept .well-known discovery requests
38
+ if (url.includes('.well-known/')) {
39
+ // If we have custom endpoints, return synthetic metadata
40
+ if (oauthApp?.authorization_endpoint && oauthApp?.token_endpoint) {
41
+ const metadata = {
42
+ issuer: new URL(oauthApp.authorization_endpoint).origin,
43
+ authorization_endpoint: oauthApp.authorization_endpoint,
44
+ token_endpoint: oauthApp.token_endpoint,
45
+ response_types_supported: ['code'],
46
+ code_challenge_methods_supported: ['S256'],
47
+ };
48
+ return new Response(JSON.stringify(metadata), {
49
+ status: 200,
50
+ headers: { 'Content-Type': 'application/json' },
51
+ });
52
+ }
53
+ // Otherwise, try the real endpoint but convert 5xx to 404
54
+ const res = await fetch(input, init);
55
+ if (res.status >= 500) {
56
+ return new Response(null, { status: 404 });
57
+ }
58
+ return res;
59
+ }
60
+ return fetch(input, init);
61
+ };
62
+ }
63
+ import { SOURCE_DEFINITIONS } from '../../services/sync/source-definitions/definitions.js';
64
+ import { join } from 'path';
65
+ /** Check if the current user is an admin (or API key mode). */
66
+ function isAdmin(req) {
67
+ const user = req.user;
68
+ if (!user)
69
+ return true; // API key / open mode
70
+ return user.role === 'admin';
71
+ }
72
+ /** Check if user can access a repo (admin, or has collection access, or owns private collection). */
73
+ function hasRepoAccess(req, repoName, workspaceManager, authService) {
74
+ const user = req.user;
75
+ if (!user)
76
+ return true; // API key / open mode
77
+ const repoConfig = workspaceManager.getRepoConfig(repoName);
78
+ if (repoConfig?.private && repoConfig?.owner_id === user.id)
79
+ return true;
80
+ if (user.role === 'admin')
81
+ return true;
82
+ return authService.getUserCollections(user.id).includes(repoName);
83
+ }
84
+ /** Check if user can run sync on a repo (admin for shared, owner for private). */
85
+ function canSync(req, repoName, workspaceManager, authService) {
86
+ const user = req.user;
87
+ if (!user)
88
+ return true; // API key / open mode
89
+ const repoConfig = workspaceManager.getRepoConfig(repoName);
90
+ if (repoConfig?.private && repoConfig?.owner_id === user.id)
91
+ return true;
92
+ return user.role === 'admin';
93
+ }
94
+ export async function registerSyncApiRoutes(fastify, workspaceManager, authService, workspacePath, wsHub) {
95
+ // POST /api/repos/:repo/sync — run all sync rules that apply to a collection
96
+ // (collection-level rules + any workspace-level rules targeting this collection)
97
+ fastify.post('/api/repos/:repo/sync', async (req, reply) => {
98
+ const { repo } = req.params;
99
+ if (!canSync(req, repo, workspaceManager, authService)) {
100
+ return reply.status(403).send({ error: 'Only admins can sync shared collections' });
101
+ }
102
+ const actor = req.user?.displayName ?? 'sync';
103
+ const progress = (msg) => {
104
+ console.log(`[sync] ${msg}`);
105
+ wsHub?.broadcast({ type: 'sync_progress', repo, message: msg, actor, timestamp: new Date().toISOString() });
106
+ };
107
+ try {
108
+ const workspace = new Workspace(workspacePath);
109
+ const config = await workspace.loadConfig();
110
+ const user = req.user;
111
+ const { collectionResults, workspaceResults } = await syncRepo({
112
+ workspacePath,
113
+ collectionName: repo,
114
+ workspaceConfig: config,
115
+ schemaExtensions: config.schema_extensions,
116
+ onProgress: progress,
117
+ userId: user?.id,
118
+ authService,
119
+ serverUrl: await getServerUrl(req),
120
+ });
121
+ const allCreated = collectionResults.reduce((s, r) => s + r.created, 0) +
122
+ workspaceResults.reduce((s, r) => r.targets.reduce((ts, t) => ts + t.created, 0) + s, 0);
123
+ const sources = [
124
+ ...collectionResults.map(r => r.source),
125
+ ...workspaceResults.map(r => r.source),
126
+ ].join(', ') || repo;
127
+ wsHub?.broadcast({
128
+ type: 'sync_complete',
129
+ repo,
130
+ source: sources,
131
+ created: allCreated,
132
+ updated: 0,
133
+ deleted: 0,
134
+ actor,
135
+ timestamp: new Date().toISOString(),
136
+ });
137
+ return { success: true, collectionResults, workspaceResults };
138
+ }
139
+ catch (err) {
140
+ return reply.status(400).send({
141
+ success: false,
142
+ error: err instanceof Error ? err.message : 'Unknown error',
143
+ });
144
+ }
145
+ });
146
+ // POST /api/sync — run all sync rules (collection-level + workspace-level)
147
+ fastify.post('/api/sync', async (req, reply) => {
148
+ if (!isAdmin(req)) {
149
+ return reply.status(403).send({ error: 'Admin access required' });
150
+ }
151
+ const user = req.user;
152
+ const actor = user?.displayName ?? 'sync';
153
+ try {
154
+ const workspace = new Workspace(workspacePath);
155
+ const config = await workspace.loadConfig();
156
+ const sUrl = await getServerUrl(req);
157
+ const workspaceResults = [];
158
+ // All events from the global sync are keyed to 'workspace' so the sidebar
159
+ // toast (started with startSyncToast('workspace')) receives progress updates.
160
+ const broadcast = (msg) => {
161
+ console.log(`[sync] ${msg}`);
162
+ wsHub?.broadcast({ type: 'sync_progress', repo: 'workspace', message: msg, actor, timestamp: new Date().toISOString() });
163
+ };
164
+ const authCtx = { userId: user?.id, authService, serverUrl: sUrl };
165
+ // Group collection-level rules by (source, entity_type) so each source is
166
+ // fetched exactly once and results are distributed to all matching collections.
167
+ const grouped = new Map();
168
+ for (const repo of config.repos) {
169
+ const collectionRules = repo.sync;
170
+ if (!collectionRules || collectionRules.length === 0)
171
+ continue;
172
+ for (const rule of collectionRules) {
173
+ const key = `${rule.source}::${rule.entity_type ?? ''}`;
174
+ if (!grouped.has(key)) {
175
+ grouped.set(key, { source: rule.source, entity_type: rule.entity_type, targets: [] });
176
+ }
177
+ grouped.get(key).targets.push({ repo: repo.name, filter: rule.filter });
178
+ }
179
+ }
180
+ // Run each group as a single workspace rule (one fetch, N writes)
181
+ for (const syntheticRule of grouped.values()) {
182
+ try {
183
+ const result = await syncWorkspaceRule({
184
+ workspacePath,
185
+ rule: syntheticRule,
186
+ workspaceConfig: config,
187
+ schemaExtensions: config.schema_extensions,
188
+ onProgress: broadcast,
189
+ ...authCtx,
190
+ });
191
+ workspaceResults.push(result);
192
+ }
193
+ catch (err) {
194
+ const msg = err instanceof Error ? err.message : String(err);
195
+ workspaceResults.push({ source: syntheticRule.source, targets: syntheticRule.targets.map(t => ({ repo: t.repo, created: 0, skipped: 0, derived: 0, errors: [msg] })) });
196
+ }
197
+ }
198
+ // Run explicit workspace-level rules from config.sync
199
+ for (const rule of config.sync ?? []) {
200
+ try {
201
+ const result = await syncWorkspaceRule({
202
+ workspacePath,
203
+ rule,
204
+ workspaceConfig: config,
205
+ schemaExtensions: config.schema_extensions,
206
+ onProgress: broadcast,
207
+ ...authCtx,
208
+ });
209
+ workspaceResults.push(result);
210
+ }
211
+ catch (err) {
212
+ const msg = err instanceof Error ? err.message : String(err);
213
+ workspaceResults.push({ source: rule.source, targets: rule.targets.map(t => ({ repo: t.repo, created: 0, skipped: 0, derived: 0, errors: [msg] })) });
214
+ }
215
+ }
216
+ return { success: true, collectionResults: [], workspaceResults };
217
+ }
218
+ catch (err) {
219
+ return reply.status(400).send({
220
+ success: false,
221
+ error: err instanceof Error ? err.message : 'Unknown error',
222
+ });
223
+ }
224
+ });
225
+ // POST /api/repos/:repo/entities/:type/:id/refresh — get refresh diff
226
+ fastify.post('/api/repos/:repo/entities/:type/:id/refresh', async (req, reply) => {
227
+ const { repo, type, id } = req.params;
228
+ if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
229
+ return reply.status(403).send({ error: 'Access denied' });
230
+ }
231
+ try {
232
+ const workspace = new Workspace(workspacePath);
233
+ const config = await workspace.loadConfig();
234
+ const user = req.user;
235
+ const result = await refreshEntity({
236
+ workspacePath,
237
+ collectionName: repo,
238
+ entityType: type,
239
+ entityId: id,
240
+ workspaceConfig: config,
241
+ schemaExtensions: config.schema_extensions,
242
+ apply: false,
243
+ userId: user?.id,
244
+ authService,
245
+ serverUrl: getServerUrl(req),
246
+ });
247
+ return { success: true, result };
248
+ }
249
+ catch (err) {
250
+ return reply.status(400).send({
251
+ success: false,
252
+ error: err instanceof Error ? err.message : 'Unknown error',
253
+ });
254
+ }
255
+ });
256
+ // POST /api/repos/:repo/entities/:type/:id/refresh/apply — apply refresh
257
+ fastify.post('/api/repos/:repo/entities/:type/:id/refresh/apply', async (req, reply) => {
258
+ const { repo, type, id } = req.params;
259
+ if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
260
+ return reply.status(403).send({ error: 'Access denied' });
261
+ }
262
+ try {
263
+ const workspace = new Workspace(workspacePath);
264
+ const config = await workspace.loadConfig();
265
+ const user = req.user;
266
+ const result = await refreshEntity({
267
+ workspacePath,
268
+ collectionName: repo,
269
+ entityType: type,
270
+ entityId: id,
271
+ workspaceConfig: config,
272
+ schemaExtensions: config.schema_extensions,
273
+ apply: true,
274
+ userId: user?.id,
275
+ authService,
276
+ serverUrl: getServerUrl(req),
277
+ });
278
+ return { success: true, result };
279
+ }
280
+ catch (err) {
281
+ return reply.status(400).send({
282
+ success: false,
283
+ error: err instanceof Error ? err.message : 'Unknown error',
284
+ });
285
+ }
286
+ });
287
+ // GET /api/repos/:repo/sync/status — sync config and last sync time
288
+ fastify.get('/api/repos/:repo/sync/status', async (req, reply) => {
289
+ const { repo } = req.params;
290
+ if (!hasRepoAccess(req, repo, workspaceManager, authService)) {
291
+ return reply.status(403).send({ error: 'Access denied' });
292
+ }
293
+ try {
294
+ const workspace = new Workspace(workspacePath);
295
+ const config = await workspace.loadConfig();
296
+ const repoConfig = workspaceManager.getRepoConfig(repo);
297
+ const syncRules = repoConfig?.sync;
298
+ // Get synced entity count and last sync time
299
+ let syncedCount = 0;
300
+ let lastSync = null;
301
+ try {
302
+ const graph = workspaceManager.getGraph(repo);
303
+ const entities = graph.list();
304
+ for (const entity of entities) {
305
+ const syncedAt = entity.data?.synced_at;
306
+ if (syncedAt) {
307
+ syncedCount++;
308
+ if (!lastSync || syncedAt > lastSync)
309
+ lastSync = syncedAt;
310
+ }
311
+ }
312
+ }
313
+ catch {
314
+ // collection may be empty
315
+ }
316
+ const workspaceSyncRules = (config.sync ?? []).filter(r => r.targets.some(t => t.repo === repo));
317
+ return {
318
+ success: true,
319
+ collection: repo,
320
+ syncRules: syncRules ?? [],
321
+ workspaceSyncRules,
322
+ syncedCount,
323
+ lastSync,
324
+ };
325
+ }
326
+ catch (err) {
327
+ return reply.status(400).send({
328
+ success: false,
329
+ error: err instanceof Error ? err.message : 'Unknown error',
330
+ });
331
+ }
332
+ });
333
+ // GET /api/sources — list all source definitions
334
+ fastify.get('/api/sources', async (req, reply) => {
335
+ if (!isAdmin(req)) {
336
+ return reply.status(403).send({ error: 'Admin access required' });
337
+ }
338
+ const configMgr = new SourceConfigManager(workspacePath);
339
+ const sources = [];
340
+ const connectorConfigs = ConnectorManager.loadConfigs(workspacePath);
341
+ // Scan on-disk source files
342
+ const { readdirSync, existsSync } = await import('fs');
343
+ const sourcesDir = join(workspacePath, '.studiograph', 'sources');
344
+ const seen = new Set();
345
+ if (existsSync(sourcesDir)) {
346
+ for (const file of readdirSync(sourcesDir)) {
347
+ if (!file.endsWith('.json'))
348
+ continue;
349
+ const name = file.replace('.json', '');
350
+ seen.add(name);
351
+ const def = configMgr.getDefinition(name);
352
+ if (def) {
353
+ sources.push({
354
+ name,
355
+ format: 'new',
356
+ connector: def.connector,
357
+ entityTypes: Object.keys(def.entities),
358
+ localOnly: false,
359
+ filterFields: def.filter_fields ?? [],
360
+ configured: connectorConfigs.some(c => c.name === def.connector),
361
+ });
362
+ }
363
+ else {
364
+ const old = configMgr.get(name);
365
+ if (old) {
366
+ sources.push({
367
+ name: old.name,
368
+ format: 'legacy',
369
+ connector: old.connector,
370
+ entityTypes: old.entity_mappings.map(m => m.entity_type),
371
+ localOnly: false,
372
+ filterFields: SOURCE_DEFINITIONS[old.connector]?.filter_fields ?? [],
373
+ configured: connectorConfigs.some(c => c.name === old.connector),
374
+ });
375
+ }
376
+ }
377
+ }
378
+ }
379
+ // Add built-in templates that don't have an on-disk file
380
+ for (const [name, def] of Object.entries(SOURCE_DEFINITIONS)) {
381
+ if (seen.has(name))
382
+ continue;
383
+ sources.push({
384
+ name,
385
+ format: 'new',
386
+ connector: def.connector,
387
+ entityTypes: Object.keys(def.entities),
388
+ localOnly: false,
389
+ filterFields: def.filter_fields ?? [],
390
+ configured: connectorConfigs.some(c => c.name === def.connector),
391
+ });
392
+ }
393
+ return { success: true, sources };
394
+ });
395
+ // GET /api/sources/:name — get a single source definition (full JSON)
396
+ fastify.get('/api/sources/:name', async (req, reply) => {
397
+ if (!isAdmin(req)) {
398
+ return reply.status(403).send({ error: 'Admin access required' });
399
+ }
400
+ const { name } = req.params;
401
+ const configMgr = new SourceConfigManager(workspacePath);
402
+ const def = configMgr.getDefinition(name);
403
+ if (def) {
404
+ return { success: true, source: def };
405
+ }
406
+ const old = configMgr.get(name);
407
+ if (old) {
408
+ return { success: true, source: old };
409
+ }
410
+ return reply.status(404).send({ success: false, error: `Source "${name}" not found` });
411
+ });
412
+ // POST /api/sources/:name — create or update a source definition
413
+ fastify.post('/api/sources/:name', async (req, reply) => {
414
+ if (!isAdmin(req)) {
415
+ return reply.status(403).send({ error: 'Admin access required' });
416
+ }
417
+ const { name } = req.params;
418
+ const body = req.body;
419
+ const configMgr = new SourceConfigManager(workspacePath);
420
+ // Auto-create from built-in template
421
+ if (body && body.fromTemplate === true) {
422
+ const template = SOURCE_DEFINITIONS[name];
423
+ if (!template) {
424
+ return reply.status(400).send({
425
+ success: false,
426
+ error: `No built-in template found for "${name}"`,
427
+ });
428
+ }
429
+ if (!template.entities || Object.keys(template.entities).length === 0) {
430
+ return reply.status(400).send({
431
+ success: false,
432
+ error: `Template for "${name}" has no entity configs`,
433
+ });
434
+ }
435
+ const savedDef = { ...template, name, enabled: true, added_at: new Date().toISOString() };
436
+ configMgr.saveDefinition(name, savedDef);
437
+ return { success: true, message: `Source "${name}" created from template` };
438
+ }
439
+ // Standard create/update with full definition
440
+ const validation = configMgr.validateDefinition(body);
441
+ if (!validation.valid) {
442
+ return reply.status(400).send({
443
+ success: false,
444
+ errors: validation.errors,
445
+ });
446
+ }
447
+ configMgr.saveDefinition(name, body);
448
+ return { success: true, message: `Source "${name}" saved` };
449
+ });
450
+ // DELETE /api/sources/:name — remove a source definition
451
+ fastify.delete('/api/sources/:name', async (req, reply) => {
452
+ if (!isAdmin(req)) {
453
+ return reply.status(403).send({ error: 'Admin access required' });
454
+ }
455
+ const { name } = req.params;
456
+ const configMgr = new SourceConfigManager(workspacePath);
457
+ const removed = configMgr.remove(name);
458
+ if (removed) {
459
+ return { success: true, message: `Source "${name}" removed` };
460
+ }
461
+ return reply.status(404).send({ success: false, error: `Source "${name}" not found` });
462
+ });
463
+ // GET /api/connectors — list known + installed MCP connectors with status
464
+ fastify.get('/api/connectors', async (req, reply) => {
465
+ const user = req.user;
466
+ const userId = user?.id;
467
+ const configMgr = new SourceConfigManager(workspacePath);
468
+ const installedConfigs = ConnectorManager.loadConfigs(workspacePath);
469
+ const installedMap = new Map(installedConfigs.map(c => [c.name, c]));
470
+ const connectors = [];
471
+ // 1. All known connectors (always listed)
472
+ for (const [name, known] of Object.entries(KNOWN_CONNECTORS)) {
473
+ const installed = installedMap.get(name);
474
+ const hasSource = configMgr.getDefinition(name) !== null || configMgr.get(name) !== null;
475
+ const template = SOURCE_DEFINITIONS[name];
476
+ const hasTemplate = !!(template?.entities && Object.keys(template.entities).length > 0);
477
+ // Connected if: OAuth tokens exist for this user, or manual headers present
478
+ let connected = false;
479
+ if (known.auth === 'oauth' && userId) {
480
+ connected = authService.hasOAuthTokens(userId, name);
481
+ }
482
+ else if (known.auth === 'none') {
483
+ connected = !!installed; // local connectors are "connected" when configured
484
+ }
485
+ else if (installed?.headers?.['Authorization']) {
486
+ connected = true;
487
+ }
488
+ connectors.push({
489
+ name,
490
+ url: known.url,
491
+ description: installed?.description || known.description,
492
+ configured: !!installed,
493
+ connected,
494
+ auth: known.auth,
495
+ hasSource,
496
+ hasTemplate,
497
+ isKnown: true,
498
+ });
499
+ installedMap.delete(name);
500
+ }
501
+ // 2. Custom user-added connectors (not in KNOWN_CONNECTORS)
502
+ for (const config of installedMap.values()) {
503
+ const hasSource = configMgr.getDefinition(config.name) !== null || configMgr.get(config.name) !== null;
504
+ const template = SOURCE_DEFINITIONS[config.name];
505
+ const hasTemplate = !!(template?.entities && Object.keys(template.entities).length > 0);
506
+ connectors.push({
507
+ name: config.name,
508
+ url: config.url,
509
+ description: config.description || '',
510
+ configured: true,
511
+ connected: !!config.headers?.['Authorization'],
512
+ auth: config.auth ?? 'headers',
513
+ hasSource,
514
+ hasTemplate,
515
+ isKnown: false,
516
+ });
517
+ }
518
+ return { success: true, connectors };
519
+ });
520
+ // GET /api/connectors/:name/config — get connector config with masked secrets
521
+ fastify.get('/api/connectors/:name/config', async (req, reply) => {
522
+ if (!isAdmin(req)) {
523
+ return reply.status(403).send({ error: 'Admin access required' });
524
+ }
525
+ const { name } = req.params;
526
+ const config = ConnectorManager.loadConfig(name, workspacePath);
527
+ if (!config) {
528
+ return { success: true, config: null };
529
+ }
530
+ // Mask Authorization header
531
+ const masked = { ...config };
532
+ if (masked.headers) {
533
+ masked.headers = { ...masked.headers };
534
+ for (const key of Object.keys(masked.headers)) {
535
+ if (key.toLowerCase() === 'authorization') {
536
+ masked.headers[key] = '••••••••';
537
+ }
538
+ }
539
+ }
540
+ return { success: true, config: masked };
541
+ });
542
+ // POST /api/connectors/:name/config — create or update connector config
543
+ fastify.post('/api/connectors/:name/config', async (req, reply) => {
544
+ if (!isAdmin(req)) {
545
+ return reply.status(403).send({ error: 'Admin access required' });
546
+ }
547
+ const { name } = req.params;
548
+ const body = req.body;
549
+ if (!body || !body.url) {
550
+ return reply.status(400).send({ error: 'url is required' });
551
+ }
552
+ const config = {
553
+ name,
554
+ url: body.url,
555
+ headers: body.headers,
556
+ description: body.description ?? name,
557
+ };
558
+ ConnectorManager.saveConfig(config);
559
+ ConnectorManager.saveWorkspaceConfig(config, workspacePath);
560
+ // Auto-create source definition from template if one exists
561
+ const template = SOURCE_DEFINITIONS[name];
562
+ if (template) {
563
+ const configMgr = new SourceConfigManager(workspacePath);
564
+ const existingSource = configMgr.getDefinition(name);
565
+ if (!existingSource && template.entities && Object.keys(template.entities).length > 0) {
566
+ const savedDef = { ...template, name, enabled: true, added_at: new Date().toISOString() };
567
+ configMgr.saveDefinition(name, savedDef);
568
+ }
569
+ }
570
+ return { success: true, message: `Connector "${name}" configured` };
571
+ });
572
+ // DELETE /api/connectors/:name/config — remove connector config
573
+ fastify.delete('/api/connectors/:name/config', async (req, reply) => {
574
+ if (!isAdmin(req)) {
575
+ return reply.status(403).send({ error: 'Admin access required' });
576
+ }
577
+ const { name } = req.params;
578
+ const removed = ConnectorManager.removeConfig(name, workspacePath);
579
+ if (removed) {
580
+ return { success: true, message: `Connector "${name}" removed` };
581
+ }
582
+ return reply.status(404).send({ success: false, error: `No config for "${name}"` });
583
+ });
584
+ // ── OAuth flow routes ────────────────────────────────────────────────────
585
+ /** Resolve the server's public URL for OAuth callbacks. Uses request headers so it works on both localhost and production. */
586
+ function getServerUrl(req) {
587
+ const proto = req.headers['x-forwarded-proto'] || req.protocol || 'http';
588
+ const host = req.headers['x-forwarded-host'] || req.headers.host || req.hostname;
589
+ return `${proto}://${host}`;
590
+ }
591
+ // POST /api/connectors/:name/oauth/authorize — initiate OAuth flow
592
+ fastify.post('/api/connectors/:name/oauth/authorize', async (req, reply) => {
593
+ const user = req.user;
594
+ if (!user)
595
+ return reply.status(401).send({ error: 'Authentication required' });
596
+ const { name } = req.params;
597
+ const known = KNOWN_CONNECTORS[name];
598
+ if (!known) {
599
+ return reply.status(400).send({ error: `Unknown connector "${name}". OAuth is only available for known connectors.` });
600
+ }
601
+ // Ensure connector config exists
602
+ let config = ConnectorManager.loadConfig(name, workspacePath);
603
+ if (!config) {
604
+ config = { name, url: known.url, auth: 'oauth', description: known.description };
605
+ ConnectorManager.saveConfig(config);
606
+ ConnectorManager.saveWorkspaceConfig(config, workspacePath);
607
+ }
608
+ const serverUrl = getServerUrl(req);
609
+ const provider = new ServerOAuthProvider(name, user.id, authService, serverUrl, known.oauth);
610
+ try {
611
+ const result = await mcpAuth(provider, { serverUrl: known.url, fetchFn: createOAuthFetch(known.oauth) });
612
+ if (result === 'AUTHORIZED') {
613
+ // Already authorized (tokens valid)
614
+ return { success: true, connected: true };
615
+ }
616
+ // result === 'REDIRECT' — provider.authorizationUrl is set
617
+ if (!provider.authorizationUrl) {
618
+ return reply.status(500).send({ error: 'OAuth flow did not produce an authorization URL' });
619
+ }
620
+ return { success: true, authorizationUrl: provider.authorizationUrl };
621
+ }
622
+ catch (err) {
623
+ const msg = err instanceof Error ? err.message : String(err);
624
+ console.error(`[oauth] authorize error for ${name}:`, msg);
625
+ return reply.status(500).send({ error: `OAuth initialization failed: ${msg}` });
626
+ }
627
+ });
628
+ // GET /api/oauth/callback/:name — OAuth callback (browser redirect from OAuth provider)
629
+ // Public endpoint — user identity comes from the state parameter (cross-site redirects don't carry cookies).
630
+ fastify.get('/api/oauth/callback/:name', async (req, reply) => {
631
+ const { name } = req.params;
632
+ try {
633
+ const { code, state } = req.query;
634
+ const known = KNOWN_CONNECTORS[name];
635
+ if (!code) {
636
+ const error = req.query.error || 'missing authorization code';
637
+ return reply.type('text/html').send(`<html><body><h2>Authorization failed</h2><p>${error}</p><p>You can close this tab.</p></body></html>`);
638
+ }
639
+ if (!known) {
640
+ return reply.status(400).type('text/html').send(`<html><body><h2>Unknown connector</h2><p>"${name}" is not a known connector.</p></body></html>`);
641
+ }
642
+ const decoded = state ? decodeOAuthState(state) : null;
643
+ if (!decoded) {
644
+ return reply.status(400).type('text/html').send('<html><body><h2>Authorization failed</h2><p>Invalid or missing state parameter. Please try connecting again.</p></body></html>');
645
+ }
646
+ const serverUrl = getServerUrl(req);
647
+ const provider = new ServerOAuthProvider(decoded.connectorName, decoded.userId, authService, serverUrl, known?.oauth);
648
+ await mcpAuth(provider, { serverUrl: known.url, authorizationCode: code, fetchFn: createOAuthFetch(known?.oauth) });
649
+ return reply.type('text/html').send(`<html><body><h2>Connected to ${name}</h2><p>You can close this tab and return to Studiograph.</p></body></html>`);
650
+ }
651
+ catch (err) {
652
+ const msg = err instanceof Error ? err.message : String(err);
653
+ console.error(`[oauth] callback error for ${name}:`, msg);
654
+ return reply.type('text/html').send(`<html><body><h2>Authorization failed</h2><p>${msg}</p><p>You can close this tab and try again.</p></body></html>`);
655
+ }
656
+ });
657
+ // GET /api/connectors/:name/oauth/status — poll for OAuth connection status
658
+ fastify.get('/api/connectors/:name/oauth/status', async (req, reply) => {
659
+ const user = req.user;
660
+ if (!user)
661
+ return reply.status(401).send({ error: 'Authentication required' });
662
+ const { name } = req.params;
663
+ const connected = authService.hasOAuthTokens(user.id, name);
664
+ return { connected };
665
+ });
666
+ // POST /api/connectors/:name/oauth/disconnect — clear OAuth tokens
667
+ fastify.post('/api/connectors/:name/oauth/disconnect', async (req, reply) => {
668
+ const user = req.user;
669
+ if (!user)
670
+ return reply.status(401).send({ error: 'Authentication required' });
671
+ const { name } = req.params;
672
+ authService.clearOAuthData(user.id, name);
673
+ return { success: true };
674
+ });
675
+ // GET /api/config/bundle — export all shareable workspace config (admin only)
676
+ fastify.get('/api/config/bundle', async (req, reply) => {
677
+ if (!isAdmin(req)) {
678
+ return reply.status(403).send({ error: 'Admin access required' });
679
+ }
680
+ const { readdirSync, readFileSync, existsSync } = await import('fs');
681
+ const workspace = new Workspace(workspacePath);
682
+ const config = await workspace.loadConfig();
683
+ const about = workspace.loadAbout();
684
+ const seed = authService.getSeedData();
685
+ // Source definitions
686
+ const sources = {};
687
+ const sourcesDir = join(workspacePath, '.studiograph', 'sources');
688
+ if (existsSync(sourcesDir)) {
689
+ for (const entry of readdirSync(sourcesDir)) {
690
+ if (!entry.endsWith('.json'))
691
+ continue;
692
+ try {
693
+ sources[entry.replace('.json', '')] = JSON.parse(readFileSync(join(sourcesDir, entry), 'utf-8'));
694
+ }
695
+ catch { /* skip malformed */ }
696
+ }
697
+ }
698
+ // Workspace connector configs (already stripped of secrets)
699
+ const connectors = ConnectorManager.loadWorkspaceConfigs(workspacePath);
700
+ return reply.send({ workspace: config, about, authSeed: seed, sources, connectors });
701
+ });
702
+ // POST /api/config/bundle — import config bundle from a remote admin (admin only)
703
+ fastify.post('/api/config/bundle', async (req, reply) => {
704
+ if (!isAdmin(req)) {
705
+ return reply.status(403).send({ error: 'Admin access required' });
706
+ }
707
+ const { mkdirSync, writeFileSync } = await import('fs');
708
+ const bundle = req.body;
709
+ if (!bundle) {
710
+ return reply.status(400).send({ error: 'Request body is required' });
711
+ }
712
+ const sgDir = join(workspacePath, '.studiograph');
713
+ const applied = [];
714
+ // 1. Workspace config — merge: keep server-local fields, update shared fields
715
+ if (bundle.workspace) {
716
+ const workspace = new Workspace(workspacePath);
717
+ const local = await workspace.loadConfig().catch(() => ({}));
718
+ const merged = { ...local, ...bundle.workspace };
719
+ // Preserve server-local fields
720
+ if (local.server_url)
721
+ merged.server_url = local.server_url;
722
+ writeFileSync(join(sgDir, 'workspace.json'), JSON.stringify(merged, null, 2), 'utf-8');
723
+ applied.push('workspace');
724
+ }
725
+ // 2. ABOUT.md
726
+ if (bundle.about !== undefined && bundle.about !== null) {
727
+ writeFileSync(join(sgDir, 'ABOUT.md'), bundle.about, 'utf-8');
728
+ applied.push('about');
729
+ }
730
+ // 3. Auth seed
731
+ if (bundle.authSeed) {
732
+ writeFileSync(join(sgDir, 'auth-seed.json'), JSON.stringify(bundle.authSeed, null, 2), 'utf-8');
733
+ authService.applySeed();
734
+ applied.push(`auth (${bundle.authSeed.users?.length ?? 0} users)`);
735
+ }
736
+ // 4. Sources
737
+ if (bundle.sources && Object.keys(bundle.sources).length > 0) {
738
+ const sourcesDir = join(sgDir, 'sources');
739
+ mkdirSync(sourcesDir, { recursive: true });
740
+ for (const [name, def] of Object.entries(bundle.sources)) {
741
+ writeFileSync(join(sourcesDir, `${name}.json`), JSON.stringify(def, null, 2), 'utf-8');
742
+ }
743
+ applied.push(`sources (${Object.keys(bundle.sources).join(', ')})`);
744
+ }
745
+ // 5. Connectors (sanitized)
746
+ if (bundle.connectors && bundle.connectors.length > 0) {
747
+ const connectorsDir = join(sgDir, 'connectors');
748
+ mkdirSync(connectorsDir, { recursive: true });
749
+ for (const conn of bundle.connectors) {
750
+ writeFileSync(join(connectorsDir, `${conn.name}.json`), JSON.stringify(conn, null, 2), 'utf-8');
751
+ }
752
+ applied.push(`connectors (${bundle.connectors.map((c) => c.name).join(', ')})`);
753
+ }
754
+ return reply.send({ success: true, applied });
755
+ });
756
+ }
757
+ //# sourceMappingURL=sync-api.js.map