studiograph 1.3.2 → 1.3.3-next.10
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 +8 -0
- package/dist/agent/orchestrator.js +13 -2
- package/dist/agent/orchestrator.js.map +1 -1
- 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/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/cli/commands/join.d.ts +3 -2
- package/dist/cli/commands/join.js +93 -98
- package/dist/cli/commands/join.js.map +1 -1
- package/dist/cli/commands/redeploy.js +14 -22
- package/dist/cli/commands/redeploy.js.map +1 -1
- package/dist/cli/commands/serve.js +3 -3
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/commands/sync.d.ts +4 -2
- package/dist/cli/commands/sync.js +21 -22
- package/dist/cli/commands/sync.js.map +1 -1
- package/dist/cli/commands/user.d.ts +7 -0
- package/dist/cli/commands/user.js +153 -0
- package/dist/cli/commands/user.js.map +1 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/index.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 +9 -0
- package/dist/core/types.js +1 -0
- 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.js +1 -1
- package/dist/core/workspace.js.map +1 -1
- package/dist/server/chrome/chrome.js +12 -212
- package/dist/server/commit-scheduler.d.ts +39 -0
- package/dist/server/commit-scheduler.js +99 -0
- package/dist/server/commit-scheduler.js.map +1 -0
- package/dist/server/index.js +86 -46
- package/dist/server/index.js.map +1 -1
- package/dist/server/routes/auth-api.d.ts +8 -0
- package/dist/server/routes/auth-api.js +163 -0
- package/dist/server/routes/auth-api.js.map +1 -0
- package/dist/server/routes/chat.js +4 -0
- package/dist/server/routes/chat.js.map +1 -1
- package/dist/server/routes/git-http.d.ts +23 -0
- package/dist/server/routes/git-http.js +213 -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 +230 -50
- package/dist/server/routes/graph-api.js.map +1 -1
- package/dist/server/routes/permissions-api.d.ts +5 -4
- package/dist/server/routes/permissions-api.js +39 -167
- package/dist/server/routes/permissions-api.js.map +1 -1
- package/dist/server/routes/ws.d.ts +7 -0
- package/dist/server/routes/ws.js +35 -0
- package/dist/server/routes/ws.js.map +1 -0
- package/dist/server/ws-hub.d.ts +36 -0
- package/dist/server/ws-hub.js +63 -0
- package/dist/server/ws-hub.js.map +1 -0
- package/dist/services/auth-service.d.ts +77 -0
- package/dist/services/auth-service.js +302 -0
- package/dist/services/auth-service.js.map +1 -0
- package/dist/utils/git.d.ts +21 -1
- package/dist/utils/git.js +69 -3
- package/dist/utils/git.js.map +1 -1
- package/dist/web/_app/immutable/assets/{0.CDbX4Cwz.css → 0.CL-hNrKE.css} +1 -1
- package/dist/web/_app/immutable/assets/7.Cn2DG-J6.css +1 -0
- package/dist/web/_app/immutable/assets/8.Sm6jB3a0.css +1 -0
- package/dist/web/_app/immutable/assets/AppShell.RYbgfVr0.css +1 -0
- package/dist/web/_app/immutable/chunks/-lhxaNNQ.js +1 -0
- package/dist/web/_app/immutable/chunks/BFD-PG4k.js +2 -0
- package/dist/web/_app/immutable/chunks/BKhAbhZ4.js +1 -0
- package/dist/web/_app/immutable/chunks/BUuoVpOJ.js +1 -0
- package/dist/web/_app/immutable/chunks/BuHHk4nP.js +1 -0
- package/dist/web/_app/immutable/chunks/Bv8xNJQh.js +1 -0
- package/dist/web/_app/immutable/chunks/C0iyiXwO.js +1 -0
- package/dist/web/_app/immutable/chunks/CH-raHh7.js +1 -0
- package/dist/web/_app/immutable/chunks/{DTUXhwEY.js → CQo_whF8.js} +1 -1
- package/dist/web/_app/immutable/chunks/CiIF45lL.js +1 -0
- package/dist/web/_app/immutable/chunks/{DEJSHbC3.js → Cs6vwwZC.js} +1 -1
- package/dist/web/_app/immutable/chunks/{C1SF7XfX.js → DJO0wVMY.js} +4 -4
- package/dist/web/_app/immutable/chunks/DeY0oOW3.js +2 -0
- package/dist/web/_app/immutable/chunks/{BHedmkKI.js → Dj2efhG6.js} +18 -18
- package/dist/web/_app/immutable/chunks/ew-IdGn0.js +1 -0
- package/dist/web/_app/immutable/chunks/yEjjrv_c.js +23 -0
- package/dist/web/_app/immutable/entry/app.BNN66g6y.js +2 -0
- package/dist/web/_app/immutable/entry/start.CbKqTiwM.js +1 -0
- package/dist/web/_app/immutable/nodes/0.DthLeuCh.js +2 -0
- package/dist/web/_app/immutable/nodes/1.ZBkeuxO_.js +1 -0
- package/dist/web/_app/immutable/nodes/2.CioLRnGy.js +1 -0
- package/dist/web/_app/immutable/nodes/3.Df3ut4ji.js +1 -0
- package/dist/web/_app/immutable/nodes/4.HFzA-u2O.js +16 -0
- package/dist/web/_app/immutable/nodes/5.h31NMedP.js +4 -0
- package/dist/web/_app/immutable/nodes/6.MktCIoXa.js +2 -0
- package/dist/web/_app/immutable/nodes/7.CZ7lC_rb.js +1 -0
- package/dist/web/_app/immutable/nodes/8.CRf2WFmY.js +1 -0
- package/dist/web/_app/version.json +1 -1
- package/dist/web/index.html +12 -12
- package/package.json +11 -2
- package/dist/web/_app/immutable/assets/AppShell.D0rmbdqF.css +0 -1
- package/dist/web/_app/immutable/chunks/Bopa-Ask.js +0 -1
- package/dist/web/_app/immutable/chunks/CEkx7wvp.js +0 -1
- package/dist/web/_app/immutable/chunks/COwytaCP.js +0 -1
- package/dist/web/_app/immutable/chunks/Dml-u95b.js +0 -2
- package/dist/web/_app/immutable/chunks/DvKVaE7M.js +0 -1
- package/dist/web/_app/immutable/chunks/J4wxg_sP.js +0 -23
- 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.B0KkA_jR.js +0 -2
- package/dist/web/_app/immutable/entry/start.DFSNI2p-.js +0 -1
- package/dist/web/_app/immutable/nodes/0.DfbCOBhn.js +0 -2
- package/dist/web/_app/immutable/nodes/1.DMtWWiM4.js +0 -1
- package/dist/web/_app/immutable/nodes/2.CgKSJOen.js +0 -1
- package/dist/web/_app/immutable/nodes/3.CVYHBZE3.js +0 -1
- package/dist/web/_app/immutable/nodes/4.CHN1uWec.js +0 -16
- package/dist/web/_app/immutable/nodes/5.B4_87Wva.js +0 -4
- package/dist/web/_app/immutable/nodes/6.CGZ970f8.js +0 -2
|
@@ -1,176 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Permissions API routes
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Self-hosted per-collection access management backed by AuthService.
|
|
5
|
+
* All endpoints require admin role.
|
|
6
6
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// GET /api/permissions/role — current user's org role and login
|
|
21
|
-
fastify.get('/api/permissions/role', async (_req, reply) => {
|
|
22
|
-
const stored = loadToken();
|
|
23
|
-
const service = getGitHubService(workspaceConfig);
|
|
24
|
-
if (!service) {
|
|
25
|
-
return reply.send({ role: null, login: null, reason: 'not_configured' });
|
|
26
|
-
}
|
|
27
|
-
try {
|
|
28
|
-
const role = await service.getCurrentUserRole();
|
|
29
|
-
return reply.send({ role, login: stored?.user?.login ?? null });
|
|
30
|
-
}
|
|
31
|
-
catch (err) {
|
|
32
|
-
return reply.status(500).send({ error: err.message });
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
// GET /api/permissions/members — list org members
|
|
36
|
-
fastify.get('/api/permissions/members', async (_req, reply) => {
|
|
37
|
-
const service = getGitHubService(workspaceConfig);
|
|
38
|
-
if (!service) {
|
|
39
|
-
return reply.status(400).send({ error: 'GitHub not configured' });
|
|
40
|
-
}
|
|
41
|
-
try {
|
|
42
|
-
const members = await service.listMembers();
|
|
43
|
-
return reply.send(members);
|
|
44
|
-
}
|
|
45
|
-
catch (err) {
|
|
46
|
-
return reply.status(500).send({ error: err.message });
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
// POST /api/permissions/members/invite — invite user to org
|
|
50
|
-
fastify.post('/api/permissions/members/invite', async (req, reply) => {
|
|
51
|
-
const service = getGitHubService(workspaceConfig);
|
|
52
|
-
if (!service) {
|
|
53
|
-
return reply.status(400).send({ error: 'GitHub not configured' });
|
|
54
|
-
}
|
|
55
|
-
const { username } = req.body ?? {};
|
|
56
|
-
if (!username) {
|
|
57
|
-
return reply.status(400).send({ error: 'username is required' });
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
const isMember = await service.isOrgMember(username);
|
|
61
|
-
if (isMember) {
|
|
62
|
-
return reply.send({ status: 'already_member' });
|
|
63
|
-
}
|
|
64
|
-
await service.inviteUser(username);
|
|
65
|
-
return reply.send({ status: 'invited' });
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
if (err?.status === 422) {
|
|
69
|
-
return reply.send({ status: 'already_invited' });
|
|
70
|
-
}
|
|
71
|
-
return reply.status(err?.status ?? 500).send({ error: err.message });
|
|
72
|
-
}
|
|
73
|
-
});
|
|
74
|
-
// DELETE /api/permissions/members/:username — remove user from org
|
|
75
|
-
fastify.delete('/api/permissions/members/:username', async (req, reply) => {
|
|
76
|
-
const service = getGitHubService(workspaceConfig);
|
|
77
|
-
if (!service) {
|
|
78
|
-
return reply.status(400).send({ error: 'GitHub not configured' });
|
|
79
|
-
}
|
|
80
|
-
try {
|
|
81
|
-
await service.removeUser(req.params.username);
|
|
82
|
-
return reply.send({ ok: true });
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
return reply.status(err?.status ?? 500).send({ error: err.message });
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
// GET /api/permissions/collections — access info for all collections
|
|
89
|
-
fastify.get('/api/permissions/collections', async (_req, reply) => {
|
|
90
|
-
const service = getGitHubService(workspaceConfig);
|
|
91
|
-
if (!service) {
|
|
92
|
-
return reply.status(400).send({ error: 'GitHub not configured' });
|
|
93
|
-
}
|
|
94
|
-
try {
|
|
95
|
-
const repos = workspaceConfig.repos ?? [];
|
|
96
|
-
const results = await Promise.all(repos
|
|
97
|
-
.filter(r => !r.private)
|
|
98
|
-
.map(async (repo) => {
|
|
99
|
-
const repoName = extractRepoName(repo.github_url) ?? repo.name;
|
|
100
|
-
try {
|
|
101
|
-
const access = await service.getRepoAccess(repoName);
|
|
102
|
-
return { collection: repo.name, repoName, ...access };
|
|
103
|
-
}
|
|
104
|
-
catch {
|
|
105
|
-
return { collection: repo.name, repoName, teams: [], collaborators: [] };
|
|
106
|
-
}
|
|
107
|
-
}));
|
|
108
|
-
return reply.send(results);
|
|
109
|
-
}
|
|
110
|
-
catch (err) {
|
|
111
|
-
return reply.status(500).send({ error: err.message });
|
|
112
|
-
}
|
|
7
|
+
export async function registerPermissionsApiRoutes(fastify, authService, workspaceManager) {
|
|
8
|
+
// GET /api/permissions/collections — all collections with their granted users
|
|
9
|
+
fastify.get('/api/permissions/collections', async (req, reply) => {
|
|
10
|
+
const user = req.user;
|
|
11
|
+
if (user && user.role !== 'admin') {
|
|
12
|
+
return reply.status(403).send({ error: 'Admin access required' });
|
|
13
|
+
}
|
|
14
|
+
const repos = workspaceManager.getAllRepoConfigs();
|
|
15
|
+
const result = repos.map(repo => ({
|
|
16
|
+
collection: repo.name,
|
|
17
|
+
users: authService.getCollectionUsers(repo.name),
|
|
18
|
+
}));
|
|
19
|
+
return reply.send(result);
|
|
113
20
|
});
|
|
114
|
-
// POST /api/permissions/
|
|
115
|
-
fastify.post('/api/permissions/
|
|
116
|
-
const
|
|
117
|
-
if (
|
|
118
|
-
return reply.status(
|
|
119
|
-
}
|
|
120
|
-
const {
|
|
121
|
-
if (!
|
|
122
|
-
return reply.status(400).send({ error: '
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return reply.status(404).send({ error: 'Collection not found' });
|
|
127
|
-
}
|
|
128
|
-
const repoName = extractRepoName(repo.github_url) ?? repo.name;
|
|
129
|
-
try {
|
|
130
|
-
const result = await service.grantAccessViaTeam(repoName, username, permission ?? 'push');
|
|
131
|
-
return reply.send(result);
|
|
132
|
-
}
|
|
133
|
-
catch (err) {
|
|
134
|
-
return reply.status(err?.status ?? 500).send({ error: err.message });
|
|
135
|
-
}
|
|
21
|
+
// POST /api/permissions/grant — grant user access to a collection
|
|
22
|
+
fastify.post('/api/permissions/grant', async (req, reply) => {
|
|
23
|
+
const user = req.user;
|
|
24
|
+
if (user && user.role !== 'admin') {
|
|
25
|
+
return reply.status(403).send({ error: 'Admin access required' });
|
|
26
|
+
}
|
|
27
|
+
const { userId, collection } = req.body ?? {};
|
|
28
|
+
if (!userId || !collection) {
|
|
29
|
+
return reply.status(400).send({ error: 'userId and collection are required' });
|
|
30
|
+
}
|
|
31
|
+
authService.grantCollectionAccess(userId, collection);
|
|
32
|
+
return reply.send({ ok: true });
|
|
136
33
|
});
|
|
137
|
-
//
|
|
138
|
-
fastify.
|
|
139
|
-
const
|
|
140
|
-
if (
|
|
141
|
-
return reply.status(
|
|
142
|
-
}
|
|
143
|
-
const
|
|
144
|
-
if (!
|
|
145
|
-
return reply.status(
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
try {
|
|
150
|
-
try {
|
|
151
|
-
await service.removeFromTeam(repoName, username);
|
|
152
|
-
}
|
|
153
|
-
catch { /* not on team */ }
|
|
154
|
-
try {
|
|
155
|
-
await service.revokeUserAccess(repoName, username);
|
|
156
|
-
}
|
|
157
|
-
catch { /* not a collaborator */ }
|
|
158
|
-
return reply.send({ ok: true });
|
|
159
|
-
}
|
|
160
|
-
catch (err) {
|
|
161
|
-
return reply.status(err?.status ?? 500).send({ error: err.message });
|
|
162
|
-
}
|
|
34
|
+
// POST /api/permissions/revoke — revoke user access to a collection
|
|
35
|
+
fastify.post('/api/permissions/revoke', async (req, reply) => {
|
|
36
|
+
const user = req.user;
|
|
37
|
+
if (user && user.role !== 'admin') {
|
|
38
|
+
return reply.status(403).send({ error: 'Admin access required' });
|
|
39
|
+
}
|
|
40
|
+
const { userId, collection } = req.body ?? {};
|
|
41
|
+
if (!userId || !collection) {
|
|
42
|
+
return reply.status(400).send({ error: 'userId and collection are required' });
|
|
43
|
+
}
|
|
44
|
+
authService.revokeCollectionAccess(userId, collection);
|
|
45
|
+
return reply.send({ ok: true });
|
|
163
46
|
});
|
|
164
47
|
}
|
|
165
|
-
/** Extract repo name from a GitHub URL like https://github.com/org/repo-name.git */
|
|
166
|
-
function extractRepoName(url) {
|
|
167
|
-
if (!url)
|
|
168
|
-
return undefined;
|
|
169
|
-
const match = url.match(/\/([^/]+?)(?:\.git)?$/);
|
|
170
|
-
return match?.[1];
|
|
171
|
-
}
|
|
172
|
-
/** Find a repo config by collection name */
|
|
173
|
-
function findRepo(config, collection) {
|
|
174
|
-
return config.repos?.find(r => r.name === collection);
|
|
175
|
-
}
|
|
176
48
|
//# sourceMappingURL=permissions-api.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"permissions-api.js","sourceRoot":"","sources":["../../../src/server/routes/permissions-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"permissions-api.js","sourceRoot":"","sources":["../../../src/server/routes/permissions-api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAMH,MAAM,CAAC,KAAK,UAAU,4BAA4B,CAChD,OAAwB,EACxB,WAAwB,EACxB,gBAAkC;IAGlC,8EAA8E;IAC9E,OAAO,CAAC,GAAG,CAAC,8BAA8B,EAAE,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;QAC/D,MAAM,IAAI,GAAI,GAAW,CAAC,IAA4B,CAAC;QACvD,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAClC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,KAAK,GAAG,gBAAgB,CAAC,iBAAiB,EAAE,CAAC;QACnD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAChC,UAAU,EAAE,IAAI,CAAC,IAAI;YACrB,KAAK,EAAE,WAAW,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC;SACjD,CAAC,CAAC,CAAC;QAEJ,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,kEAAkE;IAClE,OAAO,CAAC,IAAI,CACV,wBAAwB,EACxB,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;QACnB,MAAM,IAAI,GAAI,GAAW,CAAC,IAA4B,CAAC;QACvD,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAClC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC,CAAC;QACjF,CAAC;QAED,WAAW,CAAC,qBAAqB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACtD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC,CACF,CAAC;IAEF,oEAAoE;IACpE,OAAO,CAAC,IAAI,CACV,yBAAyB,EACzB,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;QACnB,MAAM,IAAI,GAAI,GAAW,CAAC,IAA4B,CAAC;QACvD,IAAI,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,OAAO,EAAE,CAAC;YAClC,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,uBAAuB,EAAE,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,MAAM,IAAI,CAAC,UAAU,EAAE,CAAC;YAC3B,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,oCAAoC,EAAE,CAAC,CAAC;QACjF,CAAC;QAED,WAAW,CAAC,sBAAsB,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;QACvD,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;IAClC,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket endpoint — authenticates connections and registers them with the hub.
|
|
3
|
+
*/
|
|
4
|
+
import type { FastifyInstance } from 'fastify';
|
|
5
|
+
import type { AuthService } from '../../services/auth-service.js';
|
|
6
|
+
import type { WsHub } from '../ws-hub.js';
|
|
7
|
+
export declare function registerWsRoute(fastify: FastifyInstance, wsHub: WsHub, authService: AuthService): Promise<void>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket endpoint — authenticates connections and registers them with the hub.
|
|
3
|
+
*/
|
|
4
|
+
export async function registerWsRoute(fastify, wsHub, authService) {
|
|
5
|
+
fastify.get('/ws', { websocket: true }, (socket, req) => {
|
|
6
|
+
// Authenticate via JWT cookie or ?token= query param
|
|
7
|
+
const cookies = req.cookies;
|
|
8
|
+
const jwtFromCookie = cookies?.['__sg_token'];
|
|
9
|
+
const jwtFromQuery = req.query?.token;
|
|
10
|
+
const token = jwtFromCookie || jwtFromQuery;
|
|
11
|
+
if (authService.hasUsers()) {
|
|
12
|
+
if (!token) {
|
|
13
|
+
socket.send(JSON.stringify({ error: 'Unauthorized' }));
|
|
14
|
+
socket.close(4001, 'Unauthorized');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const user = authService.verifyToken(token);
|
|
18
|
+
if (!user) {
|
|
19
|
+
socket.send(JSON.stringify({ error: 'Invalid token' }));
|
|
20
|
+
socket.close(4001, 'Invalid token');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
// Determine accessible collections for this user
|
|
24
|
+
const collections = user.role === 'admin'
|
|
25
|
+
? null // admin sees all
|
|
26
|
+
: authService.getUserCollections(user.id);
|
|
27
|
+
wsHub.addClient(socket, user, collections);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Open mode — no auth, all collections
|
|
31
|
+
wsHub.addClient(socket, null, null);
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=ws.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws.js","sourceRoot":"","sources":["../../../src/server/routes/ws.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,OAAwB,EACxB,KAAY,EACZ,WAAwB;IAExB,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,EAAE,CAAC,MAAM,EAAE,GAAG,EAAE,EAAE;QACtD,qDAAqD;QACrD,MAAM,OAAO,GAAG,GAAG,CAAC,OAA6C,CAAC;QAClE,MAAM,aAAa,GAAG,OAAO,EAAE,CAAC,YAAY,CAAC,CAAC;QAC9C,MAAM,YAAY,GAAI,GAAG,CAAC,KAAgC,EAAE,KAAK,CAAC;QAClE,MAAM,KAAK,GAAG,aAAa,IAAI,YAAY,CAAC;QAE5C,IAAI,WAAW,CAAC,QAAQ,EAAE,EAAE,CAAC;YAC3B,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC;gBACvD,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,cAAc,CAAC,CAAC;gBACnC,OAAO;YACT,CAAC;YACD,MAAM,IAAI,GAAG,WAAW,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;YAC5C,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC,CAAC;gBACxD,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,eAAe,CAAC,CAAC;gBACpC,OAAO;YACT,CAAC;YAED,iDAAiD;YACjD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,KAAK,OAAO;gBACvC,CAAC,CAAC,IAAI,CAAE,iBAAiB;gBACzB,CAAC,CAAC,WAAW,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAE5C,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC;QAC7C,CAAC;aAAM,CAAC;YACN,uCAAuC;YACvC,KAAK,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;QACtC,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Hub — event bus for real-time change notifications.
|
|
3
|
+
*
|
|
4
|
+
* Tracks authenticated WebSocket connections and broadcasts entity/repo
|
|
5
|
+
* change events scoped to each client's collection access.
|
|
6
|
+
*/
|
|
7
|
+
import type { WebSocket } from 'ws';
|
|
8
|
+
import type { AuthUser } from '../services/auth-service.js';
|
|
9
|
+
export interface WsChangeEvent {
|
|
10
|
+
type: 'entity_change';
|
|
11
|
+
action: 'created' | 'updated' | 'deleted';
|
|
12
|
+
repo: string;
|
|
13
|
+
entityType: string;
|
|
14
|
+
entityId: string;
|
|
15
|
+
actor: string;
|
|
16
|
+
source: 'api' | 'git-push';
|
|
17
|
+
timestamp: string;
|
|
18
|
+
}
|
|
19
|
+
export interface WsRepoSyncEvent {
|
|
20
|
+
type: 'repo_sync';
|
|
21
|
+
repo: string;
|
|
22
|
+
source: 'git-push';
|
|
23
|
+
actor: string;
|
|
24
|
+
timestamp: string;
|
|
25
|
+
}
|
|
26
|
+
export type WsEvent = WsChangeEvent | WsRepoSyncEvent;
|
|
27
|
+
export declare class WsHub {
|
|
28
|
+
private clients;
|
|
29
|
+
private heartbeatTimer;
|
|
30
|
+
constructor();
|
|
31
|
+
addClient(ws: WebSocket, user: AuthUser | null, collections: string[] | null): void;
|
|
32
|
+
broadcast(event: WsEvent): void;
|
|
33
|
+
get connectionCount(): number;
|
|
34
|
+
destroy(): void;
|
|
35
|
+
private canReceive;
|
|
36
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocket Hub — event bus for real-time change notifications.
|
|
3
|
+
*
|
|
4
|
+
* Tracks authenticated WebSocket connections and broadcasts entity/repo
|
|
5
|
+
* change events scoped to each client's collection access.
|
|
6
|
+
*/
|
|
7
|
+
export class WsHub {
|
|
8
|
+
clients = new Set();
|
|
9
|
+
heartbeatTimer = null;
|
|
10
|
+
constructor() {
|
|
11
|
+
// Ping all clients every 30s, drop unresponsive ones
|
|
12
|
+
this.heartbeatTimer = setInterval(() => {
|
|
13
|
+
for (const client of this.clients) {
|
|
14
|
+
if (client.ws.readyState === 1 /* OPEN */) {
|
|
15
|
+
client.ws.ping();
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
this.clients.delete(client);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}, 30_000);
|
|
22
|
+
}
|
|
23
|
+
addClient(ws, user, collections) {
|
|
24
|
+
const client = {
|
|
25
|
+
ws,
|
|
26
|
+
user,
|
|
27
|
+
collections: collections ? new Set(collections) : null,
|
|
28
|
+
};
|
|
29
|
+
this.clients.add(client);
|
|
30
|
+
ws.on('close', () => this.clients.delete(client));
|
|
31
|
+
ws.on('error', () => this.clients.delete(client));
|
|
32
|
+
}
|
|
33
|
+
broadcast(event) {
|
|
34
|
+
const payload = JSON.stringify(event);
|
|
35
|
+
for (const client of this.clients) {
|
|
36
|
+
if (client.ws.readyState !== 1 /* OPEN */)
|
|
37
|
+
continue;
|
|
38
|
+
if (!this.canReceive(client, event.repo))
|
|
39
|
+
continue;
|
|
40
|
+
client.ws.send(payload);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
get connectionCount() {
|
|
44
|
+
return this.clients.size;
|
|
45
|
+
}
|
|
46
|
+
destroy() {
|
|
47
|
+
if (this.heartbeatTimer) {
|
|
48
|
+
clearInterval(this.heartbeatTimer);
|
|
49
|
+
this.heartbeatTimer = null;
|
|
50
|
+
}
|
|
51
|
+
for (const client of this.clients) {
|
|
52
|
+
client.ws.close();
|
|
53
|
+
}
|
|
54
|
+
this.clients.clear();
|
|
55
|
+
}
|
|
56
|
+
canReceive(client, repo) {
|
|
57
|
+
// Open mode or null collections = receives everything
|
|
58
|
+
if (!client.collections)
|
|
59
|
+
return true;
|
|
60
|
+
return client.collections.has(repo);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
//# sourceMappingURL=ws-hub.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ws-hub.js","sourceRoot":"","sources":["../../src/server/ws-hub.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAgCH,MAAM,OAAO,KAAK;IACR,OAAO,GAAG,IAAI,GAAG,EAAY,CAAC;IAC9B,cAAc,GAA0C,IAAI,CAAC;IAErE;QACE,qDAAqD;QACrD,IAAI,CAAC,cAAc,GAAG,WAAW,CAAC,GAAG,EAAE;YACrC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;gBAClC,IAAI,MAAM,CAAC,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU,EAAE,CAAC;oBAC1C,MAAM,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC;gBACnB,CAAC;qBAAM,CAAC;oBACN,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;gBAC9B,CAAC;YACH,CAAC;QACH,CAAC,EAAE,MAAM,CAAC,CAAC;IACb,CAAC;IAED,SAAS,CAAC,EAAa,EAAE,IAAqB,EAAE,WAA4B;QAC1E,MAAM,MAAM,GAAa;YACvB,EAAE;YACF,IAAI;YACJ,WAAW,EAAE,WAAW,CAAC,CAAC,CAAC,IAAI,GAAG,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,IAAI;SACvD,CAAC;QACF,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAEzB,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;QAClD,EAAE,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,SAAS,CAAC,KAAc;QACtB,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;QACtC,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,IAAI,MAAM,CAAC,EAAE,CAAC,UAAU,KAAK,CAAC,CAAC,UAAU;gBAAE,SAAS;YACpD,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,CAAC;gBAAE,SAAS;YACnD,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED,IAAI,eAAe;QACjB,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC3B,CAAC;IAED,OAAO;QACL,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YACxB,aAAa,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC;YACnC,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC7B,CAAC;QACD,KAAK,MAAM,MAAM,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAClC,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;QACpB,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACvB,CAAC;IAEO,UAAU,CAAC,MAAgB,EAAE,IAAY;QAC/C,sDAAsD;QACtD,IAAI,CAAC,MAAM,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC;QACrC,OAAO,MAAM,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC;CACF"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authentication service
|
|
3
|
+
*
|
|
4
|
+
* SQLite-backed user store with bcrypt password hashing and JWT sessions.
|
|
5
|
+
* Database lives at .studiograph/auth.db inside the workspace.
|
|
6
|
+
*
|
|
7
|
+
* Auth state is portable via .studiograph/auth-seed.json — a JSON export of
|
|
8
|
+
* all users (with bcrypt hashes) and the JWT secret. The seed file is
|
|
9
|
+
* committed to the config repo so it syncs between local and Railway:
|
|
10
|
+
* local setup → commit → push → Railway redeploy → users restored from seed
|
|
11
|
+
*/
|
|
12
|
+
export interface AuthUser {
|
|
13
|
+
id: number;
|
|
14
|
+
email: string;
|
|
15
|
+
displayName: string;
|
|
16
|
+
role: 'admin' | 'member';
|
|
17
|
+
createdAt: string;
|
|
18
|
+
}
|
|
19
|
+
export interface AuthSeed {
|
|
20
|
+
jwtSecret: string;
|
|
21
|
+
users: Array<{
|
|
22
|
+
email: string;
|
|
23
|
+
password_hash: string;
|
|
24
|
+
display_name: string;
|
|
25
|
+
role: string;
|
|
26
|
+
created_at: string;
|
|
27
|
+
}>;
|
|
28
|
+
collection_access?: Array<{
|
|
29
|
+
user_email: string;
|
|
30
|
+
collection_name: string;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export declare class AuthService {
|
|
34
|
+
private db;
|
|
35
|
+
private jwtSecret;
|
|
36
|
+
private sgDir;
|
|
37
|
+
constructor(workspacePath: string);
|
|
38
|
+
createUser(email: string, password: string, displayName: string, role?: 'admin' | 'member'): AuthUser;
|
|
39
|
+
authenticate(email: string, password: string): {
|
|
40
|
+
user: AuthUser;
|
|
41
|
+
token: string;
|
|
42
|
+
} | null;
|
|
43
|
+
verifyToken(token: string): AuthUser | null;
|
|
44
|
+
listUsers(): AuthUser[];
|
|
45
|
+
deleteUser(email: string): boolean;
|
|
46
|
+
updatePassword(email: string, newPassword: string): boolean;
|
|
47
|
+
grantCollectionAccess(userId: number, collectionName: string): void;
|
|
48
|
+
revokeCollectionAccess(userId: number, collectionName: string): void;
|
|
49
|
+
getUserCollections(userId: number): string[];
|
|
50
|
+
getCollectionUsers(collectionName: string): AuthUser[];
|
|
51
|
+
renameCollection(oldName: string, newName: string): void;
|
|
52
|
+
hasUsers(): boolean;
|
|
53
|
+
getUserCount(): number;
|
|
54
|
+
/**
|
|
55
|
+
* Ensure auth.db and auth-secret are in .studiograph/.gitignore.
|
|
56
|
+
* Only the seed JSON file should be committed to the config repo.
|
|
57
|
+
*/
|
|
58
|
+
private ensureGitignore;
|
|
59
|
+
/**
|
|
60
|
+
* Export all users and JWT secret to .studiograph/auth-seed.json.
|
|
61
|
+
* If .studiograph/ is a git repo with a remote, auto-commits and pushes
|
|
62
|
+
* the seed file so it syncs between environments.
|
|
63
|
+
*/
|
|
64
|
+
exportSeed(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Commit and push auth-seed.json if .studiograph/ is a git repo with a remote.
|
|
67
|
+
* Non-fatal — logs warnings on failure but never throws.
|
|
68
|
+
*/
|
|
69
|
+
private pushSeed;
|
|
70
|
+
/**
|
|
71
|
+
* Import users and JWT secret from .studiograph/auth-seed.json.
|
|
72
|
+
* Only runs when the database is empty (fresh boot / volume wipe).
|
|
73
|
+
*/
|
|
74
|
+
private importFromSeed;
|
|
75
|
+
close(): void;
|
|
76
|
+
private toAuthUser;
|
|
77
|
+
}
|