@vibescope/mcp-server 0.0.1
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/README.md +98 -0
- package/dist/cli.d.ts +34 -0
- package/dist/cli.js +356 -0
- package/dist/cli.test.d.ts +1 -0
- package/dist/cli.test.js +367 -0
- package/dist/handlers/__test-utils__.d.ts +72 -0
- package/dist/handlers/__test-utils__.js +176 -0
- package/dist/handlers/blockers.d.ts +18 -0
- package/dist/handlers/blockers.js +81 -0
- package/dist/handlers/bodies-of-work.d.ts +34 -0
- package/dist/handlers/bodies-of-work.js +614 -0
- package/dist/handlers/checkouts.d.ts +37 -0
- package/dist/handlers/checkouts.js +377 -0
- package/dist/handlers/cost.d.ts +39 -0
- package/dist/handlers/cost.js +247 -0
- package/dist/handlers/decisions.d.ts +16 -0
- package/dist/handlers/decisions.js +64 -0
- package/dist/handlers/deployment.d.ts +36 -0
- package/dist/handlers/deployment.js +1062 -0
- package/dist/handlers/discovery.d.ts +14 -0
- package/dist/handlers/discovery.js +870 -0
- package/dist/handlers/fallback.d.ts +18 -0
- package/dist/handlers/fallback.js +216 -0
- package/dist/handlers/findings.d.ts +18 -0
- package/dist/handlers/findings.js +110 -0
- package/dist/handlers/git-issues.d.ts +22 -0
- package/dist/handlers/git-issues.js +247 -0
- package/dist/handlers/ideas.d.ts +19 -0
- package/dist/handlers/ideas.js +188 -0
- package/dist/handlers/index.d.ts +29 -0
- package/dist/handlers/index.js +65 -0
- package/dist/handlers/knowledge-query.d.ts +22 -0
- package/dist/handlers/knowledge-query.js +253 -0
- package/dist/handlers/knowledge.d.ts +12 -0
- package/dist/handlers/knowledge.js +108 -0
- package/dist/handlers/milestones.d.ts +20 -0
- package/dist/handlers/milestones.js +179 -0
- package/dist/handlers/organizations.d.ts +36 -0
- package/dist/handlers/organizations.js +428 -0
- package/dist/handlers/progress.d.ts +14 -0
- package/dist/handlers/progress.js +149 -0
- package/dist/handlers/project.d.ts +20 -0
- package/dist/handlers/project.js +278 -0
- package/dist/handlers/requests.d.ts +16 -0
- package/dist/handlers/requests.js +131 -0
- package/dist/handlers/roles.d.ts +30 -0
- package/dist/handlers/roles.js +281 -0
- package/dist/handlers/session.d.ts +20 -0
- package/dist/handlers/session.js +791 -0
- package/dist/handlers/tasks.d.ts +52 -0
- package/dist/handlers/tasks.js +1111 -0
- package/dist/handlers/tasks.test.d.ts +1 -0
- package/dist/handlers/tasks.test.js +431 -0
- package/dist/handlers/types.d.ts +94 -0
- package/dist/handlers/types.js +1 -0
- package/dist/handlers/validation.d.ts +16 -0
- package/dist/handlers/validation.js +188 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2707 -0
- package/dist/knowledge.d.ts +6 -0
- package/dist/knowledge.js +121 -0
- package/dist/tools.d.ts +2 -0
- package/dist/tools.js +2498 -0
- package/dist/utils.d.ts +149 -0
- package/dist/utils.js +317 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +532 -0
- package/dist/validators.d.ts +35 -0
- package/dist/validators.js +111 -0
- package/dist/validators.test.d.ts +1 -0
- package/dist/validators.test.js +176 -0
- package/package.json +44 -0
- package/src/cli.test.ts +442 -0
- package/src/cli.ts +439 -0
- package/src/handlers/__test-utils__.ts +217 -0
- package/src/handlers/blockers.test.ts +390 -0
- package/src/handlers/blockers.ts +110 -0
- package/src/handlers/bodies-of-work.test.ts +1276 -0
- package/src/handlers/bodies-of-work.ts +783 -0
- package/src/handlers/cost.test.ts +436 -0
- package/src/handlers/cost.ts +322 -0
- package/src/handlers/decisions.test.ts +401 -0
- package/src/handlers/decisions.ts +86 -0
- package/src/handlers/deployment.test.ts +516 -0
- package/src/handlers/deployment.ts +1289 -0
- package/src/handlers/discovery.test.ts +254 -0
- package/src/handlers/discovery.ts +969 -0
- package/src/handlers/fallback.test.ts +687 -0
- package/src/handlers/fallback.ts +260 -0
- package/src/handlers/findings.test.ts +565 -0
- package/src/handlers/findings.ts +153 -0
- package/src/handlers/ideas.test.ts +753 -0
- package/src/handlers/ideas.ts +247 -0
- package/src/handlers/index.ts +69 -0
- package/src/handlers/milestones.test.ts +584 -0
- package/src/handlers/milestones.ts +217 -0
- package/src/handlers/organizations.test.ts +997 -0
- package/src/handlers/organizations.ts +550 -0
- package/src/handlers/progress.test.ts +369 -0
- package/src/handlers/progress.ts +188 -0
- package/src/handlers/project.test.ts +562 -0
- package/src/handlers/project.ts +352 -0
- package/src/handlers/requests.test.ts +531 -0
- package/src/handlers/requests.ts +150 -0
- package/src/handlers/session.test.ts +459 -0
- package/src/handlers/session.ts +912 -0
- package/src/handlers/tasks.test.ts +602 -0
- package/src/handlers/tasks.ts +1393 -0
- package/src/handlers/types.ts +88 -0
- package/src/handlers/validation.test.ts +880 -0
- package/src/handlers/validation.ts +223 -0
- package/src/index.ts +3205 -0
- package/src/knowledge.ts +132 -0
- package/src/tmpclaude-0078-cwd +1 -0
- package/src/tmpclaude-0ee1-cwd +1 -0
- package/src/tmpclaude-2dd5-cwd +1 -0
- package/src/tmpclaude-344c-cwd +1 -0
- package/src/tmpclaude-3860-cwd +1 -0
- package/src/tmpclaude-4b63-cwd +1 -0
- package/src/tmpclaude-5c73-cwd +1 -0
- package/src/tmpclaude-5ee3-cwd +1 -0
- package/src/tmpclaude-6795-cwd +1 -0
- package/src/tmpclaude-709e-cwd +1 -0
- package/src/tmpclaude-9839-cwd +1 -0
- package/src/tmpclaude-d829-cwd +1 -0
- package/src/tmpclaude-e072-cwd +1 -0
- package/src/tmpclaude-f6ee-cwd +1 -0
- package/src/utils.test.ts +681 -0
- package/src/utils.ts +375 -0
- package/src/validators.test.ts +223 -0
- package/src/validators.ts +122 -0
- package/tmpclaude-0439-cwd +1 -0
- package/tmpclaude-132f-cwd +1 -0
- package/tmpclaude-15bb-cwd +1 -0
- package/tmpclaude-165a-cwd +1 -0
- package/tmpclaude-1ba9-cwd +1 -0
- package/tmpclaude-21a3-cwd +1 -0
- package/tmpclaude-2a38-cwd +1 -0
- package/tmpclaude-2adf-cwd +1 -0
- package/tmpclaude-2f56-cwd +1 -0
- package/tmpclaude-3626-cwd +1 -0
- package/tmpclaude-3727-cwd +1 -0
- package/tmpclaude-40bc-cwd +1 -0
- package/tmpclaude-436f-cwd +1 -0
- package/tmpclaude-4783-cwd +1 -0
- package/tmpclaude-4b6d-cwd +1 -0
- package/tmpclaude-4ba4-cwd +1 -0
- package/tmpclaude-51e6-cwd +1 -0
- package/tmpclaude-5ecf-cwd +1 -0
- package/tmpclaude-6f97-cwd +1 -0
- package/tmpclaude-7fb2-cwd +1 -0
- package/tmpclaude-825c-cwd +1 -0
- package/tmpclaude-8baf-cwd +1 -0
- package/tmpclaude-8d9f-cwd +1 -0
- package/tmpclaude-975c-cwd +1 -0
- package/tmpclaude-9983-cwd +1 -0
- package/tmpclaude-a045-cwd +1 -0
- package/tmpclaude-ac4a-cwd +1 -0
- package/tmpclaude-b593-cwd +1 -0
- package/tmpclaude-b891-cwd +1 -0
- package/tmpclaude-c032-cwd +1 -0
- package/tmpclaude-cf43-cwd +1 -0
- package/tmpclaude-d040-cwd +1 -0
- package/tmpclaude-dcdd-cwd +1 -0
- package/tmpclaude-dcee-cwd +1 -0
- package/tmpclaude-e16b-cwd +1 -0
- package/tmpclaude-ecd2-cwd +1 -0
- package/tmpclaude-f48d-cwd +1 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +13 -0
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organizations Handlers
|
|
3
|
+
*
|
|
4
|
+
* Handles organization management, membership, and project sharing:
|
|
5
|
+
* - list_organizations
|
|
6
|
+
* - create_organization
|
|
7
|
+
* - update_organization
|
|
8
|
+
* - delete_organization
|
|
9
|
+
* - list_org_members
|
|
10
|
+
* - invite_member
|
|
11
|
+
* - update_member_role
|
|
12
|
+
* - remove_member
|
|
13
|
+
* - leave_organization
|
|
14
|
+
* - share_project_with_org
|
|
15
|
+
* - update_project_share
|
|
16
|
+
* - unshare_project
|
|
17
|
+
* - list_project_shares
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Handler, HandlerRegistry } from './types.js';
|
|
21
|
+
import { validateRequired, validateUUID } from '../validators.js';
|
|
22
|
+
import { randomBytes } from 'crypto';
|
|
23
|
+
|
|
24
|
+
// Valid roles in order of permission level
|
|
25
|
+
const ROLE_ORDER = ['viewer', 'member', 'admin', 'owner'] as const;
|
|
26
|
+
type Role = (typeof ROLE_ORDER)[number];
|
|
27
|
+
|
|
28
|
+
// Valid share permissions
|
|
29
|
+
const PERMISSION_ORDER = ['read', 'write', 'admin'] as const;
|
|
30
|
+
type Permission = (typeof PERMISSION_ORDER)[number];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generate a URL-friendly slug from a name
|
|
34
|
+
*/
|
|
35
|
+
function generateSlug(name: string): string {
|
|
36
|
+
return name
|
|
37
|
+
.toLowerCase()
|
|
38
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
39
|
+
.replace(/^-|-$/g, '')
|
|
40
|
+
.slice(0, 50);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Generate a secure invite token
|
|
45
|
+
*/
|
|
46
|
+
function generateInviteToken(): string {
|
|
47
|
+
return randomBytes(32).toString('base64url');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Organization Management
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
export const listOrganizations: Handler = async (_args, ctx) => {
|
|
55
|
+
const { supabase, auth } = ctx;
|
|
56
|
+
|
|
57
|
+
const { data, error } = await supabase
|
|
58
|
+
.from('organization_members')
|
|
59
|
+
.select(`
|
|
60
|
+
role,
|
|
61
|
+
joined_at,
|
|
62
|
+
organizations (
|
|
63
|
+
id,
|
|
64
|
+
name,
|
|
65
|
+
slug,
|
|
66
|
+
description,
|
|
67
|
+
logo_url,
|
|
68
|
+
owner_id,
|
|
69
|
+
created_at
|
|
70
|
+
)
|
|
71
|
+
`)
|
|
72
|
+
.eq('user_id', auth.userId)
|
|
73
|
+
.order('joined_at', { ascending: false });
|
|
74
|
+
|
|
75
|
+
if (error) throw new Error(`Failed to list organizations: ${error.message}`);
|
|
76
|
+
|
|
77
|
+
const organizations = (data || []).map((m) => ({
|
|
78
|
+
...(m.organizations as unknown as Record<string, unknown>),
|
|
79
|
+
role: m.role,
|
|
80
|
+
joined_at: m.joined_at,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
return { result: { organizations, count: organizations.length } };
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const createOrganization: Handler = async (args, ctx) => {
|
|
87
|
+
const { name, description, slug: customSlug } = args as {
|
|
88
|
+
name: string;
|
|
89
|
+
description?: string;
|
|
90
|
+
slug?: string;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
validateRequired(name, 'name');
|
|
94
|
+
|
|
95
|
+
const { supabase, auth } = ctx;
|
|
96
|
+
const slug = customSlug || generateSlug(name);
|
|
97
|
+
|
|
98
|
+
// Check if slug is available
|
|
99
|
+
const { data: existing } = await supabase
|
|
100
|
+
.from('organizations')
|
|
101
|
+
.select('id')
|
|
102
|
+
.eq('slug', slug)
|
|
103
|
+
.maybeSingle();
|
|
104
|
+
|
|
105
|
+
if (existing) {
|
|
106
|
+
throw new Error(`Organization slug "${slug}" is already taken`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const { data, error } = await supabase
|
|
110
|
+
.from('organizations')
|
|
111
|
+
.insert({
|
|
112
|
+
name,
|
|
113
|
+
slug,
|
|
114
|
+
description: description || null,
|
|
115
|
+
owner_id: auth.userId,
|
|
116
|
+
})
|
|
117
|
+
.select()
|
|
118
|
+
.single();
|
|
119
|
+
|
|
120
|
+
if (error) throw new Error(`Failed to create organization: ${error.message}`);
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
result: {
|
|
124
|
+
success: true,
|
|
125
|
+
organization: data,
|
|
126
|
+
message: `Organization "${name}" created. You are the owner.`,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
export const updateOrganization: Handler = async (args, ctx) => {
|
|
132
|
+
const { organization_id, name, description, logo_url } = args as {
|
|
133
|
+
organization_id: string;
|
|
134
|
+
name?: string;
|
|
135
|
+
description?: string;
|
|
136
|
+
logo_url?: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
validateRequired(organization_id, 'organization_id');
|
|
140
|
+
validateUUID(organization_id, 'organization_id');
|
|
141
|
+
|
|
142
|
+
const { supabase } = ctx;
|
|
143
|
+
|
|
144
|
+
const updates: Record<string, unknown> = {};
|
|
145
|
+
if (name !== undefined) updates.name = name;
|
|
146
|
+
if (description !== undefined) updates.description = description;
|
|
147
|
+
if (logo_url !== undefined) updates.logo_url = logo_url;
|
|
148
|
+
|
|
149
|
+
if (Object.keys(updates).length === 0) {
|
|
150
|
+
throw new Error('No updates provided');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const { data, error } = await supabase
|
|
154
|
+
.from('organizations')
|
|
155
|
+
.update(updates)
|
|
156
|
+
.eq('id', organization_id)
|
|
157
|
+
.select()
|
|
158
|
+
.single();
|
|
159
|
+
|
|
160
|
+
if (error) throw new Error(`Failed to update organization: ${error.message}`);
|
|
161
|
+
|
|
162
|
+
return { result: { success: true, organization: data } };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export const deleteOrganization: Handler = async (args, ctx) => {
|
|
166
|
+
const { organization_id } = args as { organization_id: string };
|
|
167
|
+
|
|
168
|
+
validateRequired(organization_id, 'organization_id');
|
|
169
|
+
validateUUID(organization_id, 'organization_id');
|
|
170
|
+
|
|
171
|
+
const { supabase } = ctx;
|
|
172
|
+
|
|
173
|
+
const { error } = await supabase
|
|
174
|
+
.from('organizations')
|
|
175
|
+
.delete()
|
|
176
|
+
.eq('id', organization_id);
|
|
177
|
+
|
|
178
|
+
if (error) throw new Error(`Failed to delete organization: ${error.message}`);
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
result: {
|
|
182
|
+
success: true,
|
|
183
|
+
message: 'Organization deleted. All shares have been removed.',
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Member Management
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
export const listOrgMembers: Handler = async (args, ctx) => {
|
|
193
|
+
const { organization_id } = args as { organization_id: string };
|
|
194
|
+
|
|
195
|
+
validateRequired(organization_id, 'organization_id');
|
|
196
|
+
validateUUID(organization_id, 'organization_id');
|
|
197
|
+
|
|
198
|
+
const { supabase } = ctx;
|
|
199
|
+
|
|
200
|
+
const { data, error } = await supabase
|
|
201
|
+
.from('organization_members')
|
|
202
|
+
.select('id, user_id, role, joined_at, invited_by')
|
|
203
|
+
.eq('organization_id', organization_id)
|
|
204
|
+
.order('role', { ascending: true })
|
|
205
|
+
.order('joined_at', { ascending: true });
|
|
206
|
+
|
|
207
|
+
if (error) throw new Error(`Failed to list members: ${error.message}`);
|
|
208
|
+
|
|
209
|
+
return { result: { members: data || [], count: data?.length || 0 } };
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const inviteMember: Handler = async (args, ctx) => {
|
|
213
|
+
const { organization_id, email, role = 'member' } = args as {
|
|
214
|
+
organization_id: string;
|
|
215
|
+
email: string;
|
|
216
|
+
role?: 'admin' | 'member' | 'viewer';
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
validateRequired(organization_id, 'organization_id');
|
|
220
|
+
validateRequired(email, 'email');
|
|
221
|
+
validateUUID(organization_id, 'organization_id');
|
|
222
|
+
|
|
223
|
+
if (!['admin', 'member', 'viewer'].includes(role)) {
|
|
224
|
+
throw new Error('Invalid role. Must be admin, member, or viewer.');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const { supabase, auth } = ctx;
|
|
228
|
+
const token = generateInviteToken();
|
|
229
|
+
|
|
230
|
+
// Check for existing pending invite
|
|
231
|
+
const { data: existing } = await supabase
|
|
232
|
+
.from('organization_invites')
|
|
233
|
+
.select('id')
|
|
234
|
+
.eq('organization_id', organization_id)
|
|
235
|
+
.eq('email', email)
|
|
236
|
+
.is('accepted_at', null)
|
|
237
|
+
.gt('expires_at', new Date().toISOString())
|
|
238
|
+
.maybeSingle();
|
|
239
|
+
|
|
240
|
+
if (existing) {
|
|
241
|
+
throw new Error(`A pending invite already exists for ${email}`);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const { data, error } = await supabase
|
|
245
|
+
.from('organization_invites')
|
|
246
|
+
.insert({
|
|
247
|
+
organization_id,
|
|
248
|
+
email,
|
|
249
|
+
role,
|
|
250
|
+
token,
|
|
251
|
+
invited_by: auth.userId,
|
|
252
|
+
})
|
|
253
|
+
.select()
|
|
254
|
+
.single();
|
|
255
|
+
|
|
256
|
+
if (error) throw new Error(`Failed to create invite: ${error.message}`);
|
|
257
|
+
|
|
258
|
+
return {
|
|
259
|
+
result: {
|
|
260
|
+
success: true,
|
|
261
|
+
invite: data,
|
|
262
|
+
message: `Invite sent to ${email} with role "${role}"`,
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
export const updateMemberRole: Handler = async (args, ctx) => {
|
|
268
|
+
const { organization_id, user_id, role } = args as {
|
|
269
|
+
organization_id: string;
|
|
270
|
+
user_id: string;
|
|
271
|
+
role: Role;
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
validateRequired(organization_id, 'organization_id');
|
|
275
|
+
validateRequired(user_id, 'user_id');
|
|
276
|
+
validateRequired(role, 'role');
|
|
277
|
+
validateUUID(organization_id, 'organization_id');
|
|
278
|
+
validateUUID(user_id, 'user_id');
|
|
279
|
+
|
|
280
|
+
if (!ROLE_ORDER.includes(role)) {
|
|
281
|
+
throw new Error(`Invalid role. Must be one of: ${ROLE_ORDER.join(', ')}`);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (role === 'owner') {
|
|
285
|
+
throw new Error('Cannot assign owner role. Use transfer ownership instead.');
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const { supabase, auth } = ctx;
|
|
289
|
+
|
|
290
|
+
// Prevent demoting yourself
|
|
291
|
+
if (user_id === auth.userId) {
|
|
292
|
+
throw new Error('Cannot change your own role');
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const { data, error } = await supabase
|
|
296
|
+
.from('organization_members')
|
|
297
|
+
.update({ role })
|
|
298
|
+
.eq('organization_id', organization_id)
|
|
299
|
+
.eq('user_id', user_id)
|
|
300
|
+
.neq('role', 'owner') // Cannot change owner's role
|
|
301
|
+
.select()
|
|
302
|
+
.single();
|
|
303
|
+
|
|
304
|
+
if (error) throw new Error(`Failed to update member role: ${error.message}`);
|
|
305
|
+
|
|
306
|
+
return { result: { success: true, member: data } };
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
export const removeMember: Handler = async (args, ctx) => {
|
|
310
|
+
const { organization_id, user_id } = args as {
|
|
311
|
+
organization_id: string;
|
|
312
|
+
user_id: string;
|
|
313
|
+
};
|
|
314
|
+
|
|
315
|
+
validateRequired(organization_id, 'organization_id');
|
|
316
|
+
validateRequired(user_id, 'user_id');
|
|
317
|
+
validateUUID(organization_id, 'organization_id');
|
|
318
|
+
validateUUID(user_id, 'user_id');
|
|
319
|
+
|
|
320
|
+
const { supabase } = ctx;
|
|
321
|
+
|
|
322
|
+
const { error } = await supabase
|
|
323
|
+
.from('organization_members')
|
|
324
|
+
.delete()
|
|
325
|
+
.eq('organization_id', organization_id)
|
|
326
|
+
.eq('user_id', user_id)
|
|
327
|
+
.neq('role', 'owner'); // Cannot remove owner
|
|
328
|
+
|
|
329
|
+
if (error) throw new Error(`Failed to remove member: ${error.message}`);
|
|
330
|
+
|
|
331
|
+
return {
|
|
332
|
+
result: {
|
|
333
|
+
success: true,
|
|
334
|
+
message: 'Member removed. Their org-scoped API keys have been invalidated.',
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
export const leaveOrganization: Handler = async (args, ctx) => {
|
|
340
|
+
const { organization_id } = args as { organization_id: string };
|
|
341
|
+
|
|
342
|
+
validateRequired(organization_id, 'organization_id');
|
|
343
|
+
validateUUID(organization_id, 'organization_id');
|
|
344
|
+
|
|
345
|
+
const { supabase, auth } = ctx;
|
|
346
|
+
|
|
347
|
+
// Check if user is owner
|
|
348
|
+
const { data: membership } = await supabase
|
|
349
|
+
.from('organization_members')
|
|
350
|
+
.select('role')
|
|
351
|
+
.eq('organization_id', organization_id)
|
|
352
|
+
.eq('user_id', auth.userId)
|
|
353
|
+
.single();
|
|
354
|
+
|
|
355
|
+
if (membership?.role === 'owner') {
|
|
356
|
+
throw new Error('Owner cannot leave. Transfer ownership first or delete the organization.');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const { error } = await supabase
|
|
360
|
+
.from('organization_members')
|
|
361
|
+
.delete()
|
|
362
|
+
.eq('organization_id', organization_id)
|
|
363
|
+
.eq('user_id', auth.userId);
|
|
364
|
+
|
|
365
|
+
if (error) throw new Error(`Failed to leave organization: ${error.message}`);
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
result: {
|
|
369
|
+
success: true,
|
|
370
|
+
message: 'You have left the organization.',
|
|
371
|
+
},
|
|
372
|
+
};
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// ============================================================================
|
|
376
|
+
// Project Sharing
|
|
377
|
+
// ============================================================================
|
|
378
|
+
|
|
379
|
+
export const shareProjectWithOrg: Handler = async (args, ctx) => {
|
|
380
|
+
const { project_id, organization_id, permission = 'read' } = args as {
|
|
381
|
+
project_id: string;
|
|
382
|
+
organization_id: string;
|
|
383
|
+
permission?: Permission;
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
validateRequired(project_id, 'project_id');
|
|
387
|
+
validateRequired(organization_id, 'organization_id');
|
|
388
|
+
validateUUID(project_id, 'project_id');
|
|
389
|
+
validateUUID(organization_id, 'organization_id');
|
|
390
|
+
|
|
391
|
+
if (!PERMISSION_ORDER.includes(permission)) {
|
|
392
|
+
throw new Error(`Invalid permission. Must be one of: ${PERMISSION_ORDER.join(', ')}`);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const { supabase, auth } = ctx;
|
|
396
|
+
|
|
397
|
+
// Verify user owns the project
|
|
398
|
+
const { data: project } = await supabase
|
|
399
|
+
.from('projects')
|
|
400
|
+
.select('id, name')
|
|
401
|
+
.eq('id', project_id)
|
|
402
|
+
.eq('user_id', auth.userId)
|
|
403
|
+
.single();
|
|
404
|
+
|
|
405
|
+
if (!project) {
|
|
406
|
+
throw new Error('Project not found or you are not the owner');
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Check if share already exists
|
|
410
|
+
const { data: existing } = await supabase
|
|
411
|
+
.from('project_shares')
|
|
412
|
+
.select('id')
|
|
413
|
+
.eq('project_id', project_id)
|
|
414
|
+
.eq('organization_id', organization_id)
|
|
415
|
+
.maybeSingle();
|
|
416
|
+
|
|
417
|
+
if (existing) {
|
|
418
|
+
throw new Error('Project is already shared with this organization');
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const { data, error } = await supabase
|
|
422
|
+
.from('project_shares')
|
|
423
|
+
.insert({
|
|
424
|
+
project_id,
|
|
425
|
+
organization_id,
|
|
426
|
+
permission,
|
|
427
|
+
shared_by: auth.userId,
|
|
428
|
+
})
|
|
429
|
+
.select()
|
|
430
|
+
.single();
|
|
431
|
+
|
|
432
|
+
if (error) throw new Error(`Failed to share project: ${error.message}`);
|
|
433
|
+
|
|
434
|
+
return {
|
|
435
|
+
result: {
|
|
436
|
+
success: true,
|
|
437
|
+
share: data,
|
|
438
|
+
message: `Project "${project.name}" shared with organization (${permission} access)`,
|
|
439
|
+
},
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export const updateProjectShare: Handler = async (args, ctx) => {
|
|
444
|
+
const { project_id, organization_id, permission } = args as {
|
|
445
|
+
project_id: string;
|
|
446
|
+
organization_id: string;
|
|
447
|
+
permission: Permission;
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
validateRequired(project_id, 'project_id');
|
|
451
|
+
validateRequired(organization_id, 'organization_id');
|
|
452
|
+
validateRequired(permission, 'permission');
|
|
453
|
+
validateUUID(project_id, 'project_id');
|
|
454
|
+
validateUUID(organization_id, 'organization_id');
|
|
455
|
+
|
|
456
|
+
if (!PERMISSION_ORDER.includes(permission)) {
|
|
457
|
+
throw new Error(`Invalid permission. Must be one of: ${PERMISSION_ORDER.join(', ')}`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const { supabase } = ctx;
|
|
461
|
+
|
|
462
|
+
const { data, error } = await supabase
|
|
463
|
+
.from('project_shares')
|
|
464
|
+
.update({ permission })
|
|
465
|
+
.eq('project_id', project_id)
|
|
466
|
+
.eq('organization_id', organization_id)
|
|
467
|
+
.select()
|
|
468
|
+
.single();
|
|
469
|
+
|
|
470
|
+
if (error) throw new Error(`Failed to update share: ${error.message}`);
|
|
471
|
+
|
|
472
|
+
return { result: { success: true, share: data } };
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
export const unshareProject: Handler = async (args, ctx) => {
|
|
476
|
+
const { project_id, organization_id } = args as {
|
|
477
|
+
project_id: string;
|
|
478
|
+
organization_id: string;
|
|
479
|
+
};
|
|
480
|
+
|
|
481
|
+
validateRequired(project_id, 'project_id');
|
|
482
|
+
validateRequired(organization_id, 'organization_id');
|
|
483
|
+
validateUUID(project_id, 'project_id');
|
|
484
|
+
validateUUID(organization_id, 'organization_id');
|
|
485
|
+
|
|
486
|
+
const { supabase } = ctx;
|
|
487
|
+
|
|
488
|
+
const { error } = await supabase
|
|
489
|
+
.from('project_shares')
|
|
490
|
+
.delete()
|
|
491
|
+
.eq('project_id', project_id)
|
|
492
|
+
.eq('organization_id', organization_id);
|
|
493
|
+
|
|
494
|
+
if (error) throw new Error(`Failed to unshare project: ${error.message}`);
|
|
495
|
+
|
|
496
|
+
return {
|
|
497
|
+
result: {
|
|
498
|
+
success: true,
|
|
499
|
+
message: 'Project share removed. Org members can no longer access this project.',
|
|
500
|
+
},
|
|
501
|
+
};
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
export const listProjectShares: Handler = async (args, ctx) => {
|
|
505
|
+
const { project_id } = args as { project_id: string };
|
|
506
|
+
|
|
507
|
+
validateRequired(project_id, 'project_id');
|
|
508
|
+
validateUUID(project_id, 'project_id');
|
|
509
|
+
|
|
510
|
+
const { supabase } = ctx;
|
|
511
|
+
|
|
512
|
+
const { data, error } = await supabase
|
|
513
|
+
.from('project_shares')
|
|
514
|
+
.select(`
|
|
515
|
+
id,
|
|
516
|
+
permission,
|
|
517
|
+
shared_at,
|
|
518
|
+
shared_by,
|
|
519
|
+
organizations (
|
|
520
|
+
id,
|
|
521
|
+
name,
|
|
522
|
+
slug
|
|
523
|
+
)
|
|
524
|
+
`)
|
|
525
|
+
.eq('project_id', project_id)
|
|
526
|
+
.order('shared_at', { ascending: false });
|
|
527
|
+
|
|
528
|
+
if (error) throw new Error(`Failed to list shares: ${error.message}`);
|
|
529
|
+
|
|
530
|
+
return { result: { shares: data || [], count: data?.length || 0 } };
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Organizations handlers registry
|
|
535
|
+
*/
|
|
536
|
+
export const organizationHandlers: HandlerRegistry = {
|
|
537
|
+
list_organizations: listOrganizations,
|
|
538
|
+
create_organization: createOrganization,
|
|
539
|
+
update_organization: updateOrganization,
|
|
540
|
+
delete_organization: deleteOrganization,
|
|
541
|
+
list_org_members: listOrgMembers,
|
|
542
|
+
invite_member: inviteMember,
|
|
543
|
+
update_member_role: updateMemberRole,
|
|
544
|
+
remove_member: removeMember,
|
|
545
|
+
leave_organization: leaveOrganization,
|
|
546
|
+
share_project_with_org: shareProjectWithOrg,
|
|
547
|
+
update_project_share: updateProjectShare,
|
|
548
|
+
unshare_project: unshareProject,
|
|
549
|
+
list_project_shares: listProjectShares,
|
|
550
|
+
};
|