@opengeni/api-router 0.30.3 → 2.2.0-canary.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/dist/api-websocket.d.ts +18 -0
  2. package/dist/app.js +1 -1
  3. package/dist/auth/managed-auth.d.ts +6 -0
  4. package/dist/{chunk-WCXHI3FF.js → chunk-3TP54PPX.js} +26692 -15389
  5. package/dist/chunk-3TP54PPX.js.map +1 -0
  6. package/dist/codemode.d.ts +8 -1
  7. package/dist/company-brain-okf.d.ts +24 -0
  8. package/dist/connected-machine-computer-access.d.ts +11 -0
  9. package/dist/connection-authority-owner.d.ts +59 -0
  10. package/dist/connection-ownership.d.ts +47 -0
  11. package/dist/controller-data-plane.d.ts +16 -0
  12. package/dist/editable-artifact-websocket.d.ts +5 -15
  13. package/dist/editable-artifact-workspace-files.d.ts +1 -1
  14. package/dist/http/api-error.d.ts +10 -0
  15. package/dist/http/cors.d.ts +5 -0
  16. package/dist/http/interaction-control-error.d.ts +13 -0
  17. package/dist/http/sse.d.ts +2 -3
  18. package/dist/index.d.ts +5 -2
  19. package/dist/index.js +326 -82
  20. package/dist/index.js.map +1 -1
  21. package/dist/integrations/atlassian.d.ts +17 -0
  22. package/dist/integrations/fiken.d.ts +7 -2
  23. package/dist/integrations/google-drive.d.ts +18 -0
  24. package/dist/integrations/oauth-client.d.ts +22 -3
  25. package/dist/integrations/oauth-profiles.d.ts +195 -0
  26. package/dist/integrations/personal-github-repositories.d.ts +45 -0
  27. package/dist/integrations/personal-github.d.ts +40 -0
  28. package/dist/integrations/pr-review-provider.d.ts +22 -0
  29. package/dist/integrations/provider-oauth.d.ts +8 -0
  30. package/dist/integrations/slack-app-home.d.ts +24 -0
  31. package/dist/integrations/slack-bot.d.ts +157 -9
  32. package/dist/integrations/slack-interactions.d.ts +39 -5
  33. package/dist/integrations/slack-routing.d.ts +120 -0
  34. package/dist/integrations/social-oauth.d.ts +5 -0
  35. package/dist/interaction-frame-proxy.d.ts +68 -0
  36. package/dist/mcp/company-brain-governed-writes.d.ts +26 -0
  37. package/dist/mcp/company-profile-agent-admin.d.ts +44 -0
  38. package/dist/mcp/documents.d.ts +1 -0
  39. package/dist/mcp/remember.d.ts +28 -0
  40. package/dist/mcp/request-abort.d.ts +19 -0
  41. package/dist/mcp/scheduled-task-view.d.ts +4 -4
  42. package/dist/mcp/server.d.ts +15 -2
  43. package/dist/mcp/session-view.d.ts +4 -0
  44. package/dist/mcp/session-wait.d.ts +137 -0
  45. package/dist/routes/automations.d.ts +4 -0
  46. package/dist/routes/billing.d.ts +5 -0
  47. package/dist/routes/browser-sessions.d.ts +4 -1
  48. package/dist/routes/company-brain.d.ts +3 -0
  49. package/dist/routes/company-profile.d.ts +2 -1
  50. package/dist/routes/connection-authorities.d.ts +3 -0
  51. package/dist/routes/personal-github-git-broker.d.ts +29 -0
  52. package/dist/routes/personal-github.d.ts +3 -0
  53. package/dist/routes/pr-review.d.ts +3 -0
  54. package/dist/routes/sessions.d.ts +1 -0
  55. package/dist/routes/user-resource-authorities.d.ts +3 -0
  56. package/dist/routes/workspace-capture.d.ts +10 -2
  57. package/dist/routes/workspace-learning.d.ts +3 -0
  58. package/dist/routes/workspaces.d.ts +19 -0
  59. package/dist/sandbox/channel-a.d.ts +11 -1
  60. package/dist/sandbox/connection-authority.d.ts +3 -0
  61. package/dist/sandbox/enrollment.d.ts +2 -0
  62. package/dist/sandbox/machines.d.ts +2 -2
  63. package/dist/sandbox/metrics-ingestion.d.ts +10 -20
  64. package/dist/sandbox/viewer.d.ts +24 -1
  65. package/dist/sandbox-file-artifacts.d.ts +11 -0
  66. package/dist/scheduled-task-deletion.d.ts +14 -0
  67. package/dist/slack-reaction-files.d.ts +1 -1
  68. package/dist/temporal-schedule-cleanup.d.ts +1 -0
  69. package/package.json +20 -18
  70. package/src/api-websocket.ts +23 -0
  71. package/src/app.ts +161 -23
  72. package/src/auth/managed-auth.ts +25 -3
  73. package/src/codemode.ts +53 -23
  74. package/src/codex-realtime.ts +8 -2
  75. package/src/company-brain-okf.ts +340 -0
  76. package/src/connected-machine-computer-access.ts +33 -0
  77. package/src/connection-authority-owner.ts +61 -0
  78. package/src/connection-ownership.ts +180 -0
  79. package/src/controller-data-plane.ts +47 -0
  80. package/src/editable-artifact-native-kernel.ts +26 -15
  81. package/src/editable-artifact-office-import.ts +28 -3
  82. package/src/editable-artifact-production.ts +13 -7
  83. package/src/editable-artifact-websocket.ts +11 -18
  84. package/src/editable-artifact-workspace-files.ts +31 -6
  85. package/src/http/api-error.ts +28 -0
  86. package/src/http/auth.ts +4 -0
  87. package/src/http/cors.ts +35 -0
  88. package/src/http/interaction-control-error.ts +164 -0
  89. package/src/http/sse.ts +112 -66
  90. package/src/index.ts +46 -6
  91. package/src/integrations/atlassian.ts +146 -35
  92. package/src/integrations/fiken.ts +102 -22
  93. package/src/integrations/google-drive.ts +301 -92
  94. package/src/integrations/oauth-client.ts +272 -143
  95. package/src/integrations/oauth-profiles.ts +477 -0
  96. package/src/integrations/personal-github-repositories.ts +445 -0
  97. package/src/integrations/personal-github.ts +705 -0
  98. package/src/integrations/pr-review-provider.ts +246 -0
  99. package/src/integrations/provider-oauth.ts +121 -5
  100. package/src/integrations/slack-app-home.ts +300 -0
  101. package/src/integrations/slack-bot.ts +342 -57
  102. package/src/integrations/slack-interactions.ts +1898 -259
  103. package/src/integrations/slack-routing.ts +337 -0
  104. package/src/integrations/social-oauth.ts +25 -0
  105. package/src/interaction-frame-proxy.ts +409 -0
  106. package/src/mcp/company-brain-governed-writes.ts +262 -0
  107. package/src/mcp/company-profile-agent-admin.ts +205 -0
  108. package/src/mcp/documents.ts +37 -6
  109. package/src/mcp/files.ts +16 -1
  110. package/src/mcp/remember.ts +181 -0
  111. package/src/mcp/request-abort.ts +44 -0
  112. package/src/mcp/scheduled-task-view.ts +1 -0
  113. package/src/mcp/server.ts +719 -369
  114. package/src/mcp/session-view.ts +54 -0
  115. package/src/mcp/session-wait.ts +554 -0
  116. package/src/model-catalog.ts +20 -12
  117. package/src/routes/api-integrations.ts +4 -0
  118. package/src/routes/api-keys.ts +1 -0
  119. package/src/routes/automations.ts +534 -0
  120. package/src/routes/billing.ts +57 -1
  121. package/src/routes/browser-sessions.ts +329 -79
  122. package/src/routes/channels.ts +17 -1
  123. package/src/routes/codex.ts +50 -0
  124. package/src/routes/company-brain.ts +367 -0
  125. package/src/routes/company-profile.ts +1 -1
  126. package/src/routes/computer-sessions.ts +320 -93
  127. package/src/routes/connection-authorities.ts +139 -0
  128. package/src/routes/connections.ts +144 -34
  129. package/src/routes/documents.ts +353 -4
  130. package/src/routes/editable-artifacts.ts +11 -3
  131. package/src/routes/enrollments.ts +116 -26
  132. package/src/routes/environments.ts +127 -55
  133. package/src/routes/files.ts +72 -11
  134. package/src/routes/install.ts +82 -28
  135. package/src/routes/integration-facets.ts +29 -0
  136. package/src/routes/interaction-resources.ts +20 -2
  137. package/src/routes/machines.ts +275 -13
  138. package/src/routes/organization-memberships.ts +717 -28
  139. package/src/routes/packs.ts +4 -3
  140. package/src/routes/personal-github-git-broker.ts +785 -0
  141. package/src/routes/personal-github.ts +319 -0
  142. package/src/routes/pr-review.ts +531 -0
  143. package/src/routes/rigs.ts +104 -39
  144. package/src/routes/scheduled-tasks.ts +19 -21
  145. package/src/routes/sessions.ts +775 -38
  146. package/src/routes/social.ts +14 -2
  147. package/src/routes/supergrok.ts +23 -4
  148. package/src/routes/transcription-recordings.ts +8 -2
  149. package/src/routes/user-resource-authorities.ts +138 -0
  150. package/src/routes/workspace-artifacts.ts +4 -1
  151. package/src/routes/workspace-capture.ts +21 -0
  152. package/src/routes/workspace-learning.ts +228 -0
  153. package/src/routes/workspaces.ts +59 -6
  154. package/src/sandbox/auth-callout.ts +38 -12
  155. package/src/sandbox/channel-a.ts +242 -19
  156. package/src/sandbox/connection-authority.ts +3 -0
  157. package/src/sandbox/enrollment.ts +15 -3
  158. package/src/sandbox/machines.ts +186 -64
  159. package/src/sandbox/metrics-ingestion.ts +220 -58
  160. package/src/sandbox/viewer.ts +243 -19
  161. package/src/sandbox-file-artifacts.ts +329 -0
  162. package/src/scheduled-task-deletion.ts +127 -0
  163. package/src/slack-reaction-files.ts +16 -5
  164. package/src/temporal-schedule-cleanup.ts +13 -0
  165. package/dist/chunk-WCXHI3FF.js.map +0 -1
@@ -1,53 +1,742 @@
1
- import { ListManagedOrganizationMembershipsResponse } from "@opengeni/contracts";
2
- import { getManagedSession, type ApiRouteDeps } from "@opengeni/core";
3
1
  import {
2
+ AddOrganizationWorkspaceMemberRequest,
3
+ AcceptOrganizationInvitationRequest,
4
+ CreateOrganizationRequest,
5
+ CreateOrganizationResponse,
6
+ CreateOrganizationWorkspaceRequest,
7
+ CreateOrganizationInvitationRequest,
8
+ ListManagedOrganizationMembershipsResponse,
9
+ ListOrganizationInvitationsPageQuery,
10
+ ListOrganizationInvitationsPageResponse,
11
+ ListOrganizationMembersResponse,
12
+ OrganizationAdministrationOverview,
13
+ OrganizationInvitation,
14
+ OrganizationMember,
15
+ OrganizationPrivateSessionSettings,
16
+ OrganizationRetentionPolicy,
17
+ OrganizationSummary,
18
+ Permission,
19
+ RevokeOrganizationInvitationRequest,
20
+ UpdateOrganizationMemberRequest,
21
+ UpdateOrganizationNameRequest,
22
+ UpdateOrganizationPrivateSessionSettingsRequest,
23
+ UpdateOrganizationRetentionPolicyRequest,
24
+ UpdateWorkspaceMemberRequest,
25
+ UpdateWorkspaceRequest,
26
+ UpdateWorkspaceSettingsRequest,
27
+ Workspace,
28
+ WorkspaceMember,
29
+ } from "@opengeni/contracts";
30
+ import {
31
+ getManagedSession,
32
+ organizationMembershipHttpStatus,
33
+ type ApiRouteDeps,
34
+ } from "@opengeni/core";
35
+ import {
36
+ acceptOrganizationInvitation,
37
+ bindPendingOrganizationInvitationsForVerifiedEmail,
38
+ createManagedOrganization,
39
+ createOrganizationSharedWorkspace,
40
+ createOrganizationInvitation,
4
41
  ensureManagedAccessForUserWithOrganizationMemberships,
42
+ getManagedUserProfilesByIds,
43
+ getOrganizationAdministrationOverview,
44
+ getOrganizationPrivateSessionSettings,
45
+ getSelfOrganizationInvitation,
46
+ getOrganizationRetentionPolicy,
47
+ listOrganizationMembers,
48
+ listOrganizationInvitations,
49
+ listSelfOrganizationInvitations,
5
50
  nestedPostgresSqlState,
51
+ revokeOrganizationInvitation,
52
+ removeWorkspaceMember,
53
+ updateOrganizationSharedWorkspace,
54
+ updateOrganizationSharedWorkspaceSettings,
55
+ updateOrganizationMember,
56
+ updateOrganizationName,
57
+ updateOrganizationPrivateSessionSettings,
58
+ updateOrganizationRetentionPolicy,
59
+ upsertOrganizationSharedWorkspaceMember,
6
60
  } from "@opengeni/db";
7
- import type { Hono } from "hono";
61
+ import type { Context, Hono } from "hono";
8
62
  import { HTTPException } from "hono/http-exception";
63
+ import { z } from "zod";
9
64
 
10
- export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDeps): void {
11
- app.get("/v1/organization-memberships", async (context) => {
12
- if (
13
- deps.settings.productAccessMode !== "managed" ||
14
- !deps.managedAuth ||
15
- !context.req.header("cookie") ||
16
- context.req.header("authorization")
17
- ) {
18
- throw new HTTPException(401, {
19
- message: "managed human session required",
20
- });
21
- }
65
+ const OrganizationId = z.string().uuid();
66
+ const WorkspaceId = z.string().uuid();
67
+ const MembershipId = z.string().uuid();
68
+ const InvitationId = z.string().uuid();
69
+
70
+ async function requireManagedHuman(context: Context, deps: ApiRouteDeps) {
71
+ if (
72
+ deps.settings.productAccessMode !== "managed" ||
73
+ !deps.managedAuth ||
74
+ !context.req.header("cookie") ||
75
+ context.req.header("authorization")
76
+ ) {
77
+ throw new HTTPException(401, { message: "managed human session required" });
78
+ }
79
+ const session = await getManagedSession(context, deps.managedAuth, {
80
+ db: deps.db,
81
+ });
82
+ if (!session?.user) {
83
+ throw new HTTPException(401, { message: "managed human session required" });
84
+ }
85
+ return { session, subjectId: `user:${session.user.id}` };
86
+ }
22
87
 
23
- const session = await getManagedSession(context, deps.managedAuth, {
24
- db: deps.db,
88
+ async function parseBody<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
89
+ const parsed = schema.safeParse(await context.req.json().catch(() => null));
90
+ if (!parsed.success) {
91
+ throw new HTTPException(422, {
92
+ message: "invalid organization membership request",
25
93
  });
26
- if (!session?.user) {
27
- throw new HTTPException(401, {
28
- message: "managed human session required",
29
- });
94
+ }
95
+ return parsed.data;
96
+ }
97
+
98
+ function parseId(schema: z.ZodString, value: string, label: string): string {
99
+ const parsed = schema.safeParse(value);
100
+ if (!parsed.success) throw new HTTPException(422, { message: `invalid ${label}` });
101
+ return parsed.data;
102
+ }
103
+
104
+ function rethrowMembershipError(error: unknown): never {
105
+ const status = organizationMembershipHttpStatus(nestedPostgresSqlState(error));
106
+ if (status !== null) {
107
+ throw new HTTPException(status, {
108
+ message:
109
+ status === 403
110
+ ? "organization administration is not authorized"
111
+ : status === 404
112
+ ? "organization resource not found"
113
+ : status === 409
114
+ ? "organization membership state changed; refresh and retry"
115
+ : "invalid organization membership operation",
116
+ });
117
+ }
118
+ throw error;
119
+ }
120
+
121
+ function parseWorkspacePermissions(permissions: string[]) {
122
+ const parsed = z.array(Permission).max(128).safeParse(permissions);
123
+ if (!parsed.success) {
124
+ throw new HTTPException(422, { message: "invalid workspace permissions" });
125
+ }
126
+ return parsed.data;
127
+ }
128
+
129
+ function normalizeAgentInstructions(value: string | null): string | null {
130
+ if (value === null) return null;
131
+ return value.trim() || null;
132
+ }
133
+
134
+ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDeps): void {
135
+ app.post("/v1/organizations", async (context) => {
136
+ const { session, subjectId } = await requireManagedHuman(context, deps);
137
+ const payload = await parseBody(context, CreateOrganizationRequest);
138
+ try {
139
+ return context.json(
140
+ CreateOrganizationResponse.parse(
141
+ await createManagedOrganization(deps.db, {
142
+ subjectId,
143
+ subjectLabel: session.user.email || session.user.name,
144
+ ...payload,
145
+ }),
146
+ ),
147
+ 201,
148
+ );
149
+ } catch (error) {
150
+ rethrowMembershipError(error);
30
151
  }
152
+ });
31
153
 
32
- let result;
154
+ app.get("/v1/organization-memberships", async (context) => {
155
+ const { session } = await requireManagedHuman(context, deps);
33
156
  try {
34
- result = await ensureManagedAccessForUserWithOrganizationMemberships(deps.db, {
157
+ const result = await ensureManagedAccessForUserWithOrganizationMemberships(deps.db, {
35
158
  userId: session.user.id,
36
159
  email: session.user.email,
37
160
  name: session.user.name,
161
+ emailVerified: session.user.emailVerified,
38
162
  });
163
+ return context.json(
164
+ ListManagedOrganizationMembershipsResponse.parse({
165
+ memberships: result.organizationMemberships,
166
+ }),
167
+ );
39
168
  } catch (error) {
40
169
  if (nestedPostgresSqlState(error) === "42501") {
41
170
  throw new HTTPException(403, {
42
171
  message: "organization membership is not active",
43
172
  });
44
173
  }
45
- throw error;
174
+ rethrowMembershipError(error);
175
+ }
176
+ });
177
+
178
+ app.get("/v1/organization-invitations", async (context) => {
179
+ const { session, subjectId } = await requireManagedHuman(context, deps);
180
+ const query = ListOrganizationInvitationsPageQuery.safeParse(context.req.query());
181
+ if (!query.success) {
182
+ throw new HTTPException(422, {
183
+ message: "invalid organization invitation page",
184
+ });
185
+ }
186
+ try {
187
+ if (session.user.emailVerified) {
188
+ await bindPendingOrganizationInvitationsForVerifiedEmail(deps.db, {
189
+ subjectId,
190
+ email: session.user.email,
191
+ });
192
+ }
193
+ return context.json(
194
+ ListOrganizationInvitationsPageResponse.parse(
195
+ await listSelfOrganizationInvitations(deps.db, {
196
+ subjectId,
197
+ ...(query.data.cursor === undefined ? {} : { cursor: query.data.cursor }),
198
+ limit: query.data.limit,
199
+ }),
200
+ ),
201
+ );
202
+ } catch (error) {
203
+ rethrowMembershipError(error);
204
+ }
205
+ });
206
+
207
+ app.get("/v1/organizations/:organizationId/overview", async (context) => {
208
+ const { subjectId } = await requireManagedHuman(context, deps);
209
+ const organizationId = parseId(
210
+ OrganizationId,
211
+ context.req.param("organizationId"),
212
+ "organization id",
213
+ );
214
+ try {
215
+ return context.json(
216
+ OrganizationAdministrationOverview.parse(
217
+ await getOrganizationAdministrationOverview(deps.db, {
218
+ organizationId,
219
+ actorSubjectId: subjectId,
220
+ }),
221
+ ),
222
+ );
223
+ } catch (error) {
224
+ rethrowMembershipError(error);
225
+ }
226
+ });
227
+
228
+ app.patch("/v1/organizations/:organizationId", async (context) => {
229
+ const { subjectId } = await requireManagedHuman(context, deps);
230
+ const organizationId = parseId(
231
+ OrganizationId,
232
+ context.req.param("organizationId"),
233
+ "organization id",
234
+ );
235
+ const payload = await parseBody(context, UpdateOrganizationNameRequest);
236
+ try {
237
+ return context.json(
238
+ OrganizationSummary.parse(
239
+ await updateOrganizationName(deps.db, {
240
+ organizationId,
241
+ actorSubjectId: subjectId,
242
+ ...payload,
243
+ }),
244
+ ),
245
+ );
246
+ } catch (error) {
247
+ rethrowMembershipError(error);
248
+ }
249
+ });
250
+
251
+ app.patch("/v1/organizations/:organizationId/workspaces/:workspaceId", async (context) => {
252
+ const { subjectId } = await requireManagedHuman(context, deps);
253
+ const organizationId = parseId(
254
+ OrganizationId,
255
+ context.req.param("organizationId"),
256
+ "organization id",
257
+ );
258
+ const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
259
+ const payload = await parseBody(context, UpdateWorkspaceRequest);
260
+ try {
261
+ return context.json(
262
+ Workspace.parse(
263
+ await updateOrganizationSharedWorkspace(deps.db, {
264
+ organizationId,
265
+ workspaceId,
266
+ actorSubjectId: subjectId,
267
+ ...(payload.name !== undefined ? { name: payload.name.trim() } : {}),
268
+ ...(payload.slug !== undefined ? { slug: payload.slug?.trim() || null } : {}),
269
+ ...(payload.agentInstructions !== undefined
270
+ ? {
271
+ agentInstructions: normalizeAgentInstructions(payload.agentInstructions),
272
+ }
273
+ : {}),
274
+ }),
275
+ ),
276
+ );
277
+ } catch (error) {
278
+ rethrowMembershipError(error);
279
+ }
280
+ });
281
+
282
+ app.post("/v1/organizations/:organizationId/workspaces", async (context) => {
283
+ const { subjectId } = await requireManagedHuman(context, deps);
284
+ const organizationId = parseId(
285
+ OrganizationId,
286
+ context.req.param("organizationId"),
287
+ "organization id",
288
+ );
289
+ const payload = await parseBody(context, CreateOrganizationWorkspaceRequest);
290
+ try {
291
+ return context.json(
292
+ Workspace.parse(
293
+ await createOrganizationSharedWorkspace(deps.db, {
294
+ organizationId,
295
+ actorSubjectId: subjectId,
296
+ name: payload.name.trim(),
297
+ ...(payload.slug !== undefined ? { slug: payload.slug?.trim() || null } : {}),
298
+ ...(payload.agentInstructions !== undefined
299
+ ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) }
300
+ : {}),
301
+ operationId: payload.operationId,
302
+ }),
303
+ ),
304
+ 201,
305
+ );
306
+ } catch (error) {
307
+ rethrowMembershipError(error);
308
+ }
309
+ });
310
+
311
+ app.patch(
312
+ "/v1/organizations/:organizationId/workspaces/:workspaceId/settings",
313
+ async (context) => {
314
+ const { subjectId } = await requireManagedHuman(context, deps);
315
+ const organizationId = parseId(
316
+ OrganizationId,
317
+ context.req.param("organizationId"),
318
+ "organization id",
319
+ );
320
+ const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
321
+ const payload = await parseBody(context, UpdateWorkspaceSettingsRequest);
322
+ try {
323
+ return context.json(
324
+ Workspace.parse(
325
+ await updateOrganizationSharedWorkspaceSettings(deps.db, {
326
+ organizationId,
327
+ workspaceId,
328
+ actorSubjectId: subjectId,
329
+ patch: payload,
330
+ }),
331
+ ),
332
+ );
333
+ } catch (error) {
334
+ rethrowMembershipError(error);
335
+ }
336
+ },
337
+ );
338
+
339
+ app.post("/v1/organizations/:organizationId/workspaces/:workspaceId/members", async (context) => {
340
+ const { subjectId } = await requireManagedHuman(context, deps);
341
+ const organizationId = parseId(
342
+ OrganizationId,
343
+ context.req.param("organizationId"),
344
+ "organization id",
345
+ );
346
+ const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
347
+ const payload = await parseBody(context, AddOrganizationWorkspaceMemberRequest);
348
+ try {
349
+ const members = await listOrganizationMembers(deps.db, {
350
+ organizationId,
351
+ actorSubjectId: subjectId,
352
+ });
353
+ const target = members.find(
354
+ (member) => member.id === payload.organizationMembershipId && member.status === "active",
355
+ );
356
+ if (!target) {
357
+ throw new HTTPException(404, {
358
+ message: "active organization member not found",
359
+ });
360
+ }
361
+ const [profile] = await getManagedUserProfilesByIds(deps.db, [
362
+ target.subjectId.slice("user:".length),
363
+ ]);
364
+ return context.json(
365
+ WorkspaceMember.parse(
366
+ await upsertOrganizationSharedWorkspaceMember(deps.db, {
367
+ organizationId,
368
+ workspaceId,
369
+ actorSubjectId: subjectId,
370
+ targetOrganizationMembershipId: target.id,
371
+ subjectLabel: profile?.email ?? profile?.name ?? null,
372
+ role: payload.role ?? "member",
373
+ permissions: parseWorkspacePermissions(payload.permissions),
374
+ requireExisting: false,
375
+ }),
376
+ ),
377
+ 201,
378
+ );
379
+ } catch (error) {
380
+ if (error instanceof HTTPException) throw error;
381
+ rethrowMembershipError(error);
382
+ }
383
+ });
384
+
385
+ app.patch(
386
+ "/v1/organizations/:organizationId/workspaces/:workspaceId/members/:subjectId",
387
+ async (context) => {
388
+ const { subjectId: actorSubjectId } = await requireManagedHuman(context, deps);
389
+ const organizationId = parseId(
390
+ OrganizationId,
391
+ context.req.param("organizationId"),
392
+ "organization id",
393
+ );
394
+ const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
395
+ const targetSubjectId = decodeURIComponent(context.req.param("subjectId"));
396
+ const payload = await parseBody(context, UpdateWorkspaceMemberRequest);
397
+ try {
398
+ const members = await listOrganizationMembers(deps.db, {
399
+ organizationId,
400
+ actorSubjectId,
401
+ });
402
+ const target = members.find(
403
+ (member) => member.subjectId === targetSubjectId && member.status === "active",
404
+ );
405
+ if (!target) {
406
+ throw new HTTPException(404, {
407
+ message: "active organization member not found",
408
+ });
409
+ }
410
+ const [profile] = await getManagedUserProfilesByIds(deps.db, [
411
+ target.subjectId.slice("user:".length),
412
+ ]);
413
+ return context.json(
414
+ WorkspaceMember.parse(
415
+ await upsertOrganizationSharedWorkspaceMember(deps.db, {
416
+ organizationId,
417
+ workspaceId,
418
+ actorSubjectId,
419
+ targetOrganizationMembershipId: target.id,
420
+ subjectLabel: profile?.email ?? profile?.name ?? null,
421
+ ...(payload.role === undefined ? {} : { role: payload.role }),
422
+ permissions: payload.permissions,
423
+ requireExisting: true,
424
+ }),
425
+ ),
426
+ );
427
+ } catch (error) {
428
+ if (error instanceof HTTPException) throw error;
429
+ rethrowMembershipError(error);
430
+ }
431
+ },
432
+ );
433
+
434
+ app.delete(
435
+ "/v1/organizations/:organizationId/workspaces/:workspaceId/members/:subjectId",
436
+ async (context) => {
437
+ const { subjectId: actorSubjectId } = await requireManagedHuman(context, deps);
438
+ const organizationId = parseId(
439
+ OrganizationId,
440
+ context.req.param("organizationId"),
441
+ "organization id",
442
+ );
443
+ const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
444
+ const targetSubjectId = decodeURIComponent(context.req.param("subjectId"));
445
+ try {
446
+ await removeWorkspaceMember(deps.db, {
447
+ accountId: organizationId,
448
+ workspaceId,
449
+ actorSubjectId,
450
+ targetSubjectId,
451
+ requireOrganizationSharedWorkspaceAdministration: true,
452
+ });
453
+ return context.body(null, 204);
454
+ } catch (error) {
455
+ rethrowMembershipError(error);
456
+ }
457
+ },
458
+ );
459
+
460
+ app.get("/v1/organizations/:organizationId/private-session-settings", async (context) => {
461
+ const { subjectId } = await requireManagedHuman(context, deps);
462
+ const organizationId = parseId(
463
+ OrganizationId,
464
+ context.req.param("organizationId"),
465
+ "organization id",
466
+ );
467
+ try {
468
+ return context.json(
469
+ OrganizationPrivateSessionSettings.parse(
470
+ await getOrganizationPrivateSessionSettings(deps.db, {
471
+ organizationId,
472
+ actorSubjectId: subjectId,
473
+ }),
474
+ ),
475
+ );
476
+ } catch (error) {
477
+ rethrowMembershipError(error);
478
+ }
479
+ });
480
+
481
+ app.patch("/v1/organizations/:organizationId/private-session-settings", async (context) => {
482
+ const { subjectId } = await requireManagedHuman(context, deps);
483
+ const organizationId = parseId(
484
+ OrganizationId,
485
+ context.req.param("organizationId"),
486
+ "organization id",
487
+ );
488
+ const payload = await parseBody(context, UpdateOrganizationPrivateSessionSettingsRequest);
489
+ try {
490
+ return context.json(
491
+ OrganizationPrivateSessionSettings.parse(
492
+ await updateOrganizationPrivateSessionSettings(deps.db, {
493
+ organizationId,
494
+ actorSubjectId: subjectId,
495
+ ...payload,
496
+ }),
497
+ ),
498
+ );
499
+ } catch (error) {
500
+ if (nestedPostgresSqlState(error) === "55000") {
501
+ throw new HTTPException(409, {
502
+ message: "private-session readiness is not activated for this organization",
503
+ });
504
+ }
505
+ rethrowMembershipError(error);
506
+ }
507
+ });
508
+
509
+ app.post("/v1/organizations/:organizationId/invitations", async (context) => {
510
+ const { subjectId } = await requireManagedHuman(context, deps);
511
+ const organizationId = parseId(
512
+ OrganizationId,
513
+ context.req.param("organizationId"),
514
+ "organization id",
515
+ );
516
+ const payload = await parseBody(context, CreateOrganizationInvitationRequest);
517
+ try {
518
+ return context.json(
519
+ OrganizationInvitation.parse(
520
+ await createOrganizationInvitation(deps.db, {
521
+ organizationId,
522
+ actorSubjectId: subjectId,
523
+ operationId: payload.operationId,
524
+ targetSubjectId: null,
525
+ targetEmail: payload.email.trim().toLowerCase(),
526
+ ...(payload.name === undefined ? {} : { targetName: payload.name }),
527
+ initialWorkspaceIds: payload.initialWorkspaceIds,
528
+ role: payload.role,
529
+ expiresAt: payload.expiresAt,
530
+ }),
531
+ ),
532
+ 201,
533
+ );
534
+ } catch (error) {
535
+ if (error instanceof HTTPException) throw error;
536
+ rethrowMembershipError(error);
537
+ }
538
+ });
539
+
540
+ app.get("/v1/organizations/:organizationId/invitations", async (context) => {
541
+ const { subjectId } = await requireManagedHuman(context, deps);
542
+ const organizationId = parseId(
543
+ OrganizationId,
544
+ context.req.param("organizationId"),
545
+ "organization id",
546
+ );
547
+ const query = ListOrganizationInvitationsPageQuery.safeParse(context.req.query());
548
+ if (!query.success) {
549
+ throw new HTTPException(422, {
550
+ message: "invalid organization invitation page",
551
+ });
552
+ }
553
+ try {
554
+ return context.json(
555
+ ListOrganizationInvitationsPageResponse.parse(
556
+ await listOrganizationInvitations(deps.db, {
557
+ organizationId,
558
+ actorSubjectId: subjectId,
559
+ ...(query.data.cursor === undefined ? {} : { cursor: query.data.cursor }),
560
+ limit: query.data.limit,
561
+ }),
562
+ ),
563
+ );
564
+ } catch (error) {
565
+ rethrowMembershipError(error);
566
+ }
567
+ });
568
+
569
+ app.post("/v1/organization-invitations/:invitationId/accept", async (context) => {
570
+ const { session, subjectId } = await requireManagedHuman(context, deps);
571
+ const invitationId = parseId(InvitationId, context.req.param("invitationId"), "invitation id");
572
+ const payload = await parseBody(context, AcceptOrganizationInvitationRequest);
573
+ try {
574
+ if (session.user.emailVerified) {
575
+ await bindPendingOrganizationInvitationsForVerifiedEmail(deps.db, {
576
+ subjectId,
577
+ email: session.user.email,
578
+ });
579
+ }
580
+ const invitation = await getSelfOrganizationInvitation(deps.db, {
581
+ subjectId,
582
+ invitationId,
583
+ });
584
+ const result = await acceptOrganizationInvitation(deps.db, {
585
+ organizationId: invitation.organizationId,
586
+ actorSubjectId: subjectId,
587
+ operationId: payload.operationId,
588
+ invitationId,
589
+ expectedRevision: payload.expectedRevision,
590
+ });
591
+ return context.json({
592
+ invitation: OrganizationInvitation.parse(result.invitation),
593
+ membership: OrganizationMember.parse(result.membership),
594
+ });
595
+ } catch (error) {
596
+ if (error instanceof HTTPException) throw error;
597
+ rethrowMembershipError(error);
46
598
  }
47
- return context.json(
48
- ListManagedOrganizationMembershipsResponse.parse({
49
- memberships: result.organizationMemberships,
50
- }),
599
+ });
600
+
601
+ app.post(
602
+ "/v1/organizations/:organizationId/invitations/:invitationId/revoke",
603
+ async (context) => {
604
+ const { subjectId } = await requireManagedHuman(context, deps);
605
+ const organizationId = parseId(
606
+ OrganizationId,
607
+ context.req.param("organizationId"),
608
+ "organization id",
609
+ );
610
+ const invitationId = parseId(
611
+ InvitationId,
612
+ context.req.param("invitationId"),
613
+ "invitation id",
614
+ );
615
+ const payload = await parseBody(context, RevokeOrganizationInvitationRequest);
616
+ try {
617
+ return context.json(
618
+ OrganizationInvitation.parse(
619
+ await revokeOrganizationInvitation(deps.db, {
620
+ organizationId,
621
+ actorSubjectId: subjectId,
622
+ operationId: payload.operationId,
623
+ invitationId,
624
+ expectedRevision: payload.expectedRevision,
625
+ }),
626
+ ),
627
+ );
628
+ } catch (error) {
629
+ rethrowMembershipError(error);
630
+ }
631
+ },
632
+ );
633
+
634
+ app.get("/v1/organizations/:organizationId/members", async (context) => {
635
+ const { subjectId } = await requireManagedHuman(context, deps);
636
+ const organizationId = parseId(
637
+ OrganizationId,
638
+ context.req.param("organizationId"),
639
+ "organization id",
51
640
  );
641
+ try {
642
+ const members = await listOrganizationMembers(deps.db, {
643
+ organizationId,
644
+ actorSubjectId: subjectId,
645
+ });
646
+ const profiles = await getManagedUserProfilesByIds(
647
+ deps.db,
648
+ members.flatMap((member) =>
649
+ member.subjectId.startsWith("user:") ? [member.subjectId.slice("user:".length)] : [],
650
+ ),
651
+ );
652
+ const profileById = new Map(profiles.map((profile) => [profile.id, profile]));
653
+ return context.json(
654
+ ListOrganizationMembersResponse.parse({
655
+ members: members.map((member) => {
656
+ const profile = profileById.get(member.subjectId.slice("user:".length));
657
+ return {
658
+ ...member,
659
+ name: profile?.name?.trim() || null,
660
+ email: profile?.email ?? null,
661
+ };
662
+ }),
663
+ }),
664
+ );
665
+ } catch (error) {
666
+ rethrowMembershipError(error);
667
+ }
668
+ });
669
+
670
+ app.patch("/v1/organizations/:organizationId/members/:membershipId", async (context) => {
671
+ const { subjectId } = await requireManagedHuman(context, deps);
672
+ const organizationId = parseId(
673
+ OrganizationId,
674
+ context.req.param("organizationId"),
675
+ "organization id",
676
+ );
677
+ const membershipId = parseId(MembershipId, context.req.param("membershipId"), "membership id");
678
+ const payload = await parseBody(context, UpdateOrganizationMemberRequest);
679
+ try {
680
+ return context.json(
681
+ OrganizationMember.parse(
682
+ await updateOrganizationMember(deps.db, {
683
+ organizationId,
684
+ actorSubjectId: subjectId,
685
+ operationId: payload.operationId,
686
+ membershipId,
687
+ transition: payload,
688
+ }),
689
+ ),
690
+ );
691
+ } catch (error) {
692
+ rethrowMembershipError(error);
693
+ }
694
+ });
695
+
696
+ app.get("/v1/organizations/:organizationId/retention-policy", async (context) => {
697
+ const { subjectId } = await requireManagedHuman(context, deps);
698
+ const organizationId = parseId(
699
+ OrganizationId,
700
+ context.req.param("organizationId"),
701
+ "organization id",
702
+ );
703
+ try {
704
+ return context.json(
705
+ OrganizationRetentionPolicy.parse(
706
+ await getOrganizationRetentionPolicy(deps.db, {
707
+ organizationId,
708
+ actorSubjectId: subjectId,
709
+ }),
710
+ ),
711
+ );
712
+ } catch (error) {
713
+ rethrowMembershipError(error);
714
+ }
715
+ });
716
+
717
+ app.patch("/v1/organizations/:organizationId/retention-policy", async (context) => {
718
+ const { subjectId } = await requireManagedHuman(context, deps);
719
+ const organizationId = parseId(
720
+ OrganizationId,
721
+ context.req.param("organizationId"),
722
+ "organization id",
723
+ );
724
+ const payload = await parseBody(context, UpdateOrganizationRetentionPolicyRequest);
725
+ try {
726
+ return context.json(
727
+ OrganizationRetentionPolicy.parse(
728
+ await updateOrganizationRetentionPolicy(deps.db, {
729
+ organizationId,
730
+ actorSubjectId: subjectId,
731
+ operationId: payload.operationId,
732
+ mode: payload.mode,
733
+ retentionDays: payload.retentionDays,
734
+ expectedVersion: payload.expectedVersion,
735
+ }),
736
+ ),
737
+ );
738
+ } catch (error) {
739
+ rethrowMembershipError(error);
740
+ }
52
741
  });
53
742
  }