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.
- package/dist/agent/orchestrator.d.ts +10 -0
- package/dist/agent/orchestrator.js +26 -7
- package/dist/agent/orchestrator.js.map +1 -1
- package/dist/agent/skills/sync-configuration.md +4 -29
- package/dist/agent/skills/sync-setup.md +2 -4
- package/dist/agent/tools/graph-tools.d.ts +5 -1
- package/dist/agent/tools/graph-tools.js +161 -9
- package/dist/agent/tools/graph-tools.js.map +1 -1
- package/dist/agent/tools/ops-tools.js +15 -126
- package/dist/agent/tools/ops-tools.js.map +1 -1
- package/dist/agent/tools/permission-tools.d.ts +15 -14
- package/dist/agent/tools/permission-tools.js +65 -128
- package/dist/agent/tools/permission-tools.js.map +1 -1
- package/dist/agent/tools/sync-tools.d.ts +7 -6
- package/dist/agent/tools/sync-tools.js +205 -178
- package/dist/agent/tools/sync-tools.js.map +1 -1
- package/dist/cli/commands/about.d.ts +13 -0
- package/dist/cli/commands/about.js +97 -0
- package/dist/cli/commands/about.js.map +1 -0
- package/dist/cli/commands/clone.d.ts +5 -2
- package/dist/cli/commands/clone.js +131 -62
- package/dist/cli/commands/clone.js.map +1 -1
- package/dist/cli/commands/connector.d.ts +2 -16
- package/dist/cli/commands/connector.js +32 -109
- package/dist/cli/commands/connector.js.map +1 -1
- package/dist/cli/commands/deploy.d.ts +0 -1
- package/dist/cli/commands/deploy.js +13 -103
- package/dist/cli/commands/deploy.js.map +1 -1
- package/dist/cli/commands/init.js +6 -93
- package/dist/cli/commands/init.js.map +1 -1
- package/dist/cli/commands/join.d.ts +3 -2
- package/dist/cli/commands/join.js +142 -97
- package/dist/cli/commands/join.js.map +1 -1
- package/dist/cli/commands/redeploy.js +1 -2
- package/dist/cli/commands/redeploy.js.map +1 -1
- package/dist/cli/commands/serve.d.ts +1 -3
- package/dist/cli/commands/serve.js +29 -109
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/start.js +1 -1
- package/dist/cli/commands/start.js.map +1 -1
- package/dist/cli/commands/sync-collection.d.ts +14 -0
- package/dist/cli/commands/sync-collection.js +366 -0
- package/dist/cli/commands/sync-collection.js.map +1 -0
- package/dist/cli/commands/sync.d.ts +4 -2
- package/dist/cli/commands/sync.js +529 -94
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/cli/index.js +15 -30
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/setup-wizard.d.ts +0 -13
- package/dist/cli/setup-wizard.js +6 -81
- package/dist/cli/setup-wizard.js.map +1 -1
- package/dist/core/graph.d.ts +8 -2
- package/dist/core/graph.js +11 -7
- package/dist/core/graph.js.map +1 -1
- package/dist/core/types.d.ts +149 -21
- package/dist/core/types.js +16 -4
- package/dist/core/types.js.map +1 -1
- package/dist/core/workspace-manager.js +1 -5
- package/dist/core/workspace-manager.js.map +1 -1
- package/dist/core/workspace.d.ts +11 -4
- package/dist/core/workspace.js +61 -26
- package/dist/core/workspace.js.map +1 -1
- package/dist/integrations/asana.d.ts +26 -0
- package/dist/integrations/asana.js +77 -0
- package/dist/integrations/asana.js.map +1 -0
- package/dist/integrations/figma-local.d.ts +16 -0
- package/dist/integrations/figma-local.js +10 -0
- package/dist/integrations/figma-local.js.map +1 -0
- package/dist/integrations/figma.d.ts +17 -0
- package/dist/integrations/figma.js +16 -0
- package/dist/integrations/figma.js.map +1 -0
- package/dist/integrations/granola.d.ts +24 -0
- package/dist/integrations/granola.js +59 -0
- package/dist/integrations/granola.js.map +1 -0
- package/dist/integrations/linear.d.ts +14 -0
- package/dist/integrations/linear.js +70 -0
- package/dist/integrations/linear.js.map +1 -0
- package/dist/integrations/paper-local.d.ts +14 -0
- package/dist/integrations/paper-local.js +10 -0
- package/dist/integrations/paper-local.js.map +1 -0
- package/dist/integrations/paper.d.ts +2 -0
- package/dist/integrations/paper.js +10 -0
- package/dist/integrations/paper.js.map +1 -0
- package/dist/integrations/pipedrive.d.ts +26 -0
- package/dist/integrations/pipedrive.js +97 -0
- package/dist/integrations/pipedrive.js.map +1 -0
- package/dist/integrations/registry.d.ts +15 -0
- package/dist/integrations/registry.js +27 -0
- package/dist/integrations/registry.js.map +1 -0
- package/dist/integrations/types.d.ts +34 -0
- package/dist/integrations/types.js +9 -0
- package/dist/integrations/types.js.map +1 -0
- package/dist/mcp/connector-manager.d.ts +45 -31
- package/dist/mcp/connector-manager.js +164 -116
- package/dist/mcp/connector-manager.js.map +1 -1
- package/dist/mcp/server-oauth-provider.d.ts +56 -0
- package/dist/mcp/server-oauth-provider.js +138 -0
- package/dist/mcp/server-oauth-provider.js.map +1 -0
- package/dist/server/chrome/chrome.css +142 -28
- package/dist/server/chrome/chrome.js +46 -220
- package/dist/server/collab-authority.d.ts +70 -0
- package/dist/server/collab-authority.js +218 -0
- package/dist/server/collab-authority.js.map +1 -0
- package/dist/server/collab.d.ts +29 -0
- package/dist/server/collab.js +195 -0
- package/dist/server/collab.js.map +1 -0
- package/dist/server/commit-scheduler.d.ts +39 -0
- package/dist/server/commit-scheduler.js +113 -0
- package/dist/server/commit-scheduler.js.map +1 -0
- package/dist/server/index.d.ts +0 -2
- package/dist/server/index.js +166 -55
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/auth-api.d.ts +6 -0
- package/dist/server/routes/auth-api.js +78 -0
- package/dist/server/routes/auth-api.js.map +1 -1
- package/dist/server/routes/chat.js +4 -0
- package/dist/server/routes/chat.js.map +1 -1
- package/dist/server/routes/collab.d.ts +6 -0
- package/dist/server/routes/collab.js +10 -0
- package/dist/server/routes/collab.js.map +1 -0
- package/dist/server/routes/git-http.d.ts +23 -0
- package/dist/server/routes/git-http.js +251 -0
- package/dist/server/routes/git-http.js.map +1 -0
- package/dist/server/routes/graph-api.d.ts +6 -2
- package/dist/server/routes/graph-api.js +266 -82
- package/dist/server/routes/graph-api.js.map +1 -1
- package/dist/server/routes/mcp.d.ts +12 -0
- package/dist/server/routes/mcp.js +35 -0
- package/dist/server/routes/mcp.js.map +1 -0
- package/dist/server/routes/permissions-api.d.ts +6 -4
- package/dist/server/routes/permissions-api.js +53 -167
- package/dist/server/routes/permissions-api.js.map +1 -1
- package/dist/server/routes/sync-api.d.ts +26 -0
- package/dist/server/routes/sync-api.js +757 -0
- package/dist/server/routes/sync-api.js.map +1 -0
- package/dist/server/routes/ws.d.ts +9 -0
- package/dist/server/routes/ws.js +131 -0
- package/dist/server/routes/ws.js.map +1 -0
- package/dist/server/session-manager.d.ts +40 -0
- package/dist/server/session-manager.js +132 -0
- package/dist/server/session-manager.js.map +1 -0
- package/dist/server/ws-hub.d.ts +130 -0
- package/dist/server/ws-hub.js +250 -0
- package/dist/server/ws-hub.js.map +1 -0
- package/dist/server/yjs-manager.d.ts +59 -0
- package/dist/server/yjs-manager.js +194 -0
- package/dist/server/yjs-manager.js.map +1 -0
- package/dist/services/auth-service.d.ts +74 -0
- package/dist/services/auth-service.js +286 -6
- package/dist/services/auth-service.js.map +1 -1
- package/dist/services/git.d.ts +6 -0
- package/dist/services/git.js +32 -2
- package/dist/services/git.js.map +1 -1
- package/dist/services/sync/collection-sync.d.ts +73 -0
- package/dist/services/sync/collection-sync.js +726 -0
- package/dist/services/sync/collection-sync.js.map +1 -0
- package/dist/services/sync/commit.js +5 -20
- package/dist/services/sync/commit.js.map +1 -1
- package/dist/services/sync/data-fetcher.d.ts +31 -0
- package/dist/services/sync/data-fetcher.js +12 -0
- package/dist/services/sync/data-fetcher.js.map +1 -0
- package/dist/services/sync/entity-refresh.d.ts +30 -0
- package/dist/services/sync/entity-refresh.js +275 -0
- package/dist/services/sync/entity-refresh.js.map +1 -0
- package/dist/services/sync/frontmatter-extractor.d.ts +2 -2
- package/dist/services/sync/frontmatter-extractor.js +1 -2
- package/dist/services/sync/frontmatter-extractor.js.map +1 -1
- package/dist/services/sync/graph-match.js +1 -1
- package/dist/services/sync/graph-match.js.map +1 -1
- package/dist/services/sync/mcp-client.d.ts +16 -4
- package/dist/services/sync/mcp-client.js +34 -20
- package/dist/services/sync/mcp-client.js.map +1 -1
- package/dist/services/sync/prompts.js +1 -1
- package/dist/services/sync/reconciler.js +1 -2
- package/dist/services/sync/reconciler.js.map +1 -1
- package/dist/services/sync/rest-client.d.ts +40 -0
- package/dist/services/sync/rest-client.js +100 -0
- package/dist/services/sync/rest-client.js.map +1 -0
- package/dist/services/sync/source-config.d.ts +23 -1
- package/dist/services/sync/source-config.js +112 -16
- package/dist/services/sync/source-config.js.map +1 -1
- package/dist/services/sync/source-definitions/asana.d.ts +3 -4
- package/dist/services/sync/source-definitions/asana.js +7 -13
- package/dist/services/sync/source-definitions/asana.js.map +1 -1
- package/dist/services/sync/source-definitions/definitions.d.ts +5 -18
- package/dist/services/sync/source-definitions/definitions.js +4 -22
- package/dist/services/sync/source-definitions/definitions.js.map +1 -1
- package/dist/services/sync/source-definitions/granola.d.ts +1 -1
- package/dist/services/sync/source-definitions/granola.js +11 -18
- package/dist/services/sync/source-definitions/granola.js.map +1 -1
- package/dist/services/sync/source-definitions/linear.d.ts +1 -1
- package/dist/services/sync/source-definitions/linear.js +17 -15
- package/dist/services/sync/source-definitions/linear.js.map +1 -1
- package/dist/services/sync/source-definitions/pipedrive.d.ts +1 -1
- package/dist/services/sync/source-definitions/pipedrive.js +6 -15
- package/dist/services/sync/source-definitions/pipedrive.js.map +1 -1
- package/dist/services/sync/staging.js +1 -2
- package/dist/services/sync/staging.js.map +1 -1
- package/dist/services/sync/structured-extractor.d.ts +8 -2
- package/dist/services/sync/structured-extractor.js +243 -35
- package/dist/services/sync/structured-extractor.js.map +1 -1
- package/dist/services/sync/types.d.ts +192 -23
- package/dist/services/sync/unstructured-extractor.d.ts +1 -3
- package/dist/services/sync/unstructured-extractor.js +2 -14
- package/dist/services/sync/unstructured-extractor.js.map +1 -1
- package/dist/utils/git.d.ts +33 -20
- package/dist/utils/git.js +119 -62
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/preflight.d.ts +1 -15
- package/dist/utils/preflight.js +1 -35
- package/dist/utils/preflight.js.map +1 -1
- package/dist/web/_app/immutable/assets/0.CupILLQs.css +1 -0
- package/dist/web/_app/immutable/assets/3.CtJi4Cy9.css +1 -0
- package/dist/web/_app/immutable/assets/5.CydFyZSu.css +1 -0
- package/dist/web/_app/immutable/assets/6.kqeOo0OW.css +1 -0
- package/dist/web/_app/immutable/assets/7.CseIx7qQ.css +1 -0
- package/dist/web/_app/immutable/assets/8.BYpFDZHK.css +1 -0
- package/dist/web/_app/immutable/assets/AppShell.Ch_ef9hJ.css +1 -0
- package/dist/web/_app/immutable/assets/ChatPanel.CP-_8txt.css +1 -0
- package/dist/web/_app/immutable/chunks/0oxpWEgM.js +1 -0
- package/dist/web/_app/immutable/chunks/B1y7Wy5O.js +18 -0
- package/dist/web/_app/immutable/chunks/B7eduG_j.js +64 -0
- package/dist/web/_app/immutable/chunks/BBLgaWN8.js +1 -0
- package/dist/web/_app/immutable/chunks/BCB5cYCz.js +2 -0
- package/dist/web/_app/immutable/chunks/{aosHekRC.js → BPUy9_sS.js} +1 -1
- package/dist/web/_app/immutable/chunks/BVBRzmeQ.js +7 -0
- package/dist/web/_app/immutable/chunks/{CUzqHQY_.js → BXuvR8Ks.js} +2 -1
- package/dist/web/_app/immutable/chunks/BeBar3OL.js +1 -0
- package/dist/web/_app/immutable/chunks/BuOTIbJu.js +1 -0
- package/dist/web/_app/immutable/chunks/CLFba8FK.js +5 -0
- package/dist/web/_app/immutable/chunks/CQCkXCml.js +1 -0
- package/dist/web/_app/immutable/chunks/CXuhHL4d.js +1 -0
- package/dist/web/_app/immutable/chunks/Cg9NOuOl.js +27 -0
- package/dist/web/_app/immutable/chunks/Cs5oz2oJ.js +5 -0
- package/dist/web/_app/immutable/chunks/Cs_ROD7H.js +2 -0
- package/dist/web/_app/immutable/chunks/D2aTbzFm.js +3 -0
- package/dist/web/_app/immutable/chunks/D4FXhiC2.js +1 -0
- package/dist/web/_app/immutable/chunks/D4VHRYeB.js +1 -0
- package/dist/web/_app/immutable/chunks/DCGSm8Hl.js +1 -0
- package/dist/web/_app/immutable/chunks/DP09rP34.js +2 -0
- package/dist/web/_app/immutable/chunks/DiP47fAp.js +1 -0
- package/dist/web/_app/immutable/chunks/DptGlK8O.js +1 -0
- package/dist/web/_app/immutable/chunks/O0fx2ss6.js +1 -0
- package/dist/web/_app/immutable/chunks/xBRYfpah.js +1 -0
- package/dist/web/_app/immutable/entry/app.FgnywZP_.js +2 -0
- package/dist/web/_app/immutable/entry/start.Bsa-zlPf.js +1 -0
- package/dist/web/_app/immutable/nodes/0.D3SW-LMc.js +10 -0
- package/dist/web/_app/immutable/nodes/1.y0c5TQTP.js +1 -0
- package/dist/web/_app/immutable/nodes/2.BQfSep9-.js +1 -0
- package/dist/web/_app/immutable/nodes/3.CC4Y-xMM.js +11 -0
- package/dist/web/_app/immutable/nodes/{5.BBpmYkAu.js → 4.Dp0Z-oPW.js} +2 -2
- package/dist/web/_app/immutable/nodes/5.gjZ03DON.js +2 -0
- package/dist/web/_app/immutable/nodes/6.dRNIwcJQ.js +1 -0
- package/dist/web/_app/immutable/nodes/7.I4Gjes3o.js +2 -0
- package/dist/web/_app/immutable/nodes/8.Dj14D7uH.js +1 -0
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +10 -12
- package/package.json +12 -2
- package/dist/web/_app/immutable/assets/0.CDbX4Cwz.css +0 -1
- package/dist/web/_app/immutable/assets/3.BJy7pVXi.css +0 -1
- package/dist/web/_app/immutable/assets/4.Ad16uh9o.css +0 -1
- package/dist/web/_app/immutable/assets/6.Bm2i7O0j.css +0 -1
- package/dist/web/_app/immutable/assets/AppShell.D0rmbdqF.css +0 -1
- package/dist/web/_app/immutable/assets/ChatPanel.RFD5GGYI.css +0 -1
- package/dist/web/_app/immutable/assets/editor.CPAf2SRV.css +0 -1
- package/dist/web/_app/immutable/chunks/479TgXB4.js +0 -1
- package/dist/web/_app/immutable/chunks/4QY4j-jX.js +0 -1
- package/dist/web/_app/immutable/chunks/BFb0g4TQ.js +0 -64
- package/dist/web/_app/immutable/chunks/Bopa-Ask.js +0 -1
- package/dist/web/_app/immutable/chunks/COwytaCP.js +0 -1
- package/dist/web/_app/immutable/chunks/DEJSHbC3.js +0 -1
- package/dist/web/_app/immutable/chunks/DNywhIex.js +0 -23
- package/dist/web/_app/immutable/chunks/DTUXhwEY.js +0 -1
- package/dist/web/_app/immutable/chunks/DThXpa0U.js +0 -6
- package/dist/web/_app/immutable/chunks/Dh_H7Owr.js +0 -18
- package/dist/web/_app/immutable/chunks/Dml-u95b.js +0 -2
- package/dist/web/_app/immutable/chunks/DnlgZ_Tk.js +0 -5
- package/dist/web/_app/immutable/chunks/DtVH--hH.js +0 -6
- package/dist/web/_app/immutable/chunks/DvKVaE7M.js +0 -1
- package/dist/web/_app/immutable/chunks/MbiSz-iW.js +0 -2
- package/dist/web/_app/immutable/chunks/bSAC733J.js +0 -1
- package/dist/web/_app/immutable/entry/app.BvodXQQ0.js +0 -2
- package/dist/web/_app/immutable/entry/start.Bkui3Kyw.js +0 -1
- package/dist/web/_app/immutable/nodes/0.DfbCOBhn.js +0 -2
- package/dist/web/_app/immutable/nodes/1.vtxUGpe6.js +0 -1
- package/dist/web/_app/immutable/nodes/2.Cq29oW4h.js +0 -1
- package/dist/web/_app/immutable/nodes/3.SquslPZy.js +0 -1
- package/dist/web/_app/immutable/nodes/4.COV8FR8b.js +0 -16
- package/dist/web/_app/immutable/nodes/6.BBbh6z9I.js +0 -2
- /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
|