@opengeni/api-router 2.3.2-canary.2 → 2.5.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 (44) hide show
  1. package/dist/app.js +1 -1
  2. package/dist/auth/managed-auth-attempt-context.d.ts +4 -0
  3. package/dist/auth/managed-auth-session-adapter.d.ts +4 -0
  4. package/dist/{chunk-IBV7Z6F4.js → chunk-QESX7HDK.js} +3645 -470
  5. package/dist/chunk-QESX7HDK.js.map +1 -0
  6. package/dist/fatal-process-boundary.d.ts +25 -0
  7. package/dist/http/sse.d.ts +2 -0
  8. package/dist/index.d.ts +5 -1
  9. package/dist/index.js +168 -6
  10. package/dist/index.js.map +1 -1
  11. package/dist/integrations/slack-interactions.d.ts +1 -1
  12. package/dist/mcp/receipts.d.ts +9 -0
  13. package/dist/mcp/server.d.ts +33 -0
  14. package/dist/organization-recovery-notifications.d.ts +41 -0
  15. package/dist/routes/managed-auth-session-sets.d.ts +19 -0
  16. package/dist/routes/organization-recovery.d.ts +19 -0
  17. package/dist/routes/sessions.d.ts +17 -5
  18. package/dist/routes/workspaces.d.ts +1 -0
  19. package/dist/work-discovery-observability.d.ts +33 -0
  20. package/package.json +18 -18
  21. package/src/app.ts +133 -1
  22. package/src/auth/managed-auth-attempt-context.ts +24 -0
  23. package/src/auth/managed-auth-session-adapter.ts +205 -0
  24. package/src/auth/managed-auth.ts +52 -2
  25. package/src/fatal-process-boundary.ts +231 -0
  26. package/src/http/sse.ts +7 -0
  27. package/src/index.ts +25 -5
  28. package/src/integrations/slack-interactions.ts +30 -17
  29. package/src/mcp/receipts.ts +34 -0
  30. package/src/mcp/server.ts +349 -68
  31. package/src/organization-recovery-notifications.ts +103 -0
  32. package/src/routes/canonical-human-identities.ts +29 -14
  33. package/src/routes/codex.ts +5 -1
  34. package/src/routes/environments.ts +23 -0
  35. package/src/routes/interaction-resources.ts +3 -0
  36. package/src/routes/managed-auth-session-sets.ts +994 -0
  37. package/src/routes/managed-onboarding.ts +2 -0
  38. package/src/routes/organization-memberships.ts +2 -0
  39. package/src/routes/organization-recovery.ts +325 -0
  40. package/src/routes/sessions.ts +401 -120
  41. package/src/routes/supergrok.ts +5 -1
  42. package/src/routes/workspaces.ts +23 -1
  43. package/src/work-discovery-observability.ts +121 -0
  44. package/dist/chunk-IBV7Z6F4.js.map +0 -1
@@ -309,6 +309,8 @@ async function requireManagedHuman(context: Context, deps: ApiRouteDeps) {
309
309
  }
310
310
  const session = await getManagedSession(context, deps.managedAuth, {
311
311
  db: deps.db,
312
+ sessionAdapter: deps.managedAuthSessionAdapter,
313
+ sessionSetMode: deps.settings.managedAuthSessionSetMode,
312
314
  });
313
315
  if (!session?.user) {
314
316
  throw new HTTPException(401, { message: "managed human session required" });
@@ -91,6 +91,8 @@ async function requireManagedHuman(context: Context, deps: ApiRouteDeps) {
91
91
  }
92
92
  const session = await getManagedSession(context, deps.managedAuth, {
93
93
  db: deps.db,
94
+ sessionAdapter: deps.managedAuthSessionAdapter,
95
+ sessionSetMode: deps.settings.managedAuthSessionSetMode,
94
96
  });
95
97
  if (!session?.user) {
96
98
  throw new HTTPException(401, { message: "managed human session required" });
@@ -0,0 +1,325 @@
1
+ import {
2
+ AcceptOrganizationRecoveryCustodyRequest,
3
+ ConfigureOrganizationRecoveryPolicyRequest,
4
+ DisableOrganizationRecoveryPolicyRequest,
5
+ OrganizationRecoveryMutationResponse,
6
+ OrganizationRecoveryOperationCommandRequest,
7
+ OrganizationRecoveryOverview,
8
+ StartOrganizationRecoveryOperationRequest,
9
+ } from "@opengeni/contracts/organization-recovery";
10
+ import {
11
+ getManagedAuthRequestActorAdmissionStamp,
12
+ getManagedAuthRequestActorLeaseStamp,
13
+ requireCanonicalHumanRequestIdentity,
14
+ } from "@opengeni/core/canonical-human-identities";
15
+ import type { ApiRouteDeps } from "@opengeni/core";
16
+ import {
17
+ acceptOrganizationRecoveryCustody,
18
+ approveOrganizationRecoveryOperation,
19
+ cancelOrganizationRecoveryOperation,
20
+ configureOrganizationRecoveryPolicy,
21
+ disableOrganizationRecoveryPolicy,
22
+ executeOrganizationRecoveryOperation,
23
+ getOrganizationRecoveryOverview,
24
+ OrganizationRecoveryDeniedError,
25
+ OrganizationRecoveryOperationReuseError,
26
+ OrganizationRecoveryRevisionConflictError,
27
+ OrganizationRecoveryUnavailableError,
28
+ startOrganizationRecoveryOperation,
29
+ } from "@opengeni/db";
30
+ import type { Context, Hono } from "hono";
31
+ import { HTTPException } from "hono/http-exception";
32
+ import { z } from "zod";
33
+ import { ApiHttpError } from "../http/api-error";
34
+
35
+ const OrganizationId = z.string().uuid();
36
+ const RecoveryOperationId = z.string().uuid();
37
+
38
+ export type OrganizationRecoveryRouteServices = {
39
+ requireCanonicalHumanRequestIdentity: typeof requireCanonicalHumanRequestIdentity;
40
+ getManagedAuthRequestActorAdmissionStamp: typeof getManagedAuthRequestActorAdmissionStamp;
41
+ getManagedAuthRequestActorLeaseStamp: typeof getManagedAuthRequestActorLeaseStamp;
42
+ getOrganizationRecoveryOverview: typeof getOrganizationRecoveryOverview;
43
+ configureOrganizationRecoveryPolicy: typeof configureOrganizationRecoveryPolicy;
44
+ acceptOrganizationRecoveryCustody: typeof acceptOrganizationRecoveryCustody;
45
+ disableOrganizationRecoveryPolicy: typeof disableOrganizationRecoveryPolicy;
46
+ startOrganizationRecoveryOperation: typeof startOrganizationRecoveryOperation;
47
+ approveOrganizationRecoveryOperation: typeof approveOrganizationRecoveryOperation;
48
+ cancelOrganizationRecoveryOperation: typeof cancelOrganizationRecoveryOperation;
49
+ executeOrganizationRecoveryOperation: typeof executeOrganizationRecoveryOperation;
50
+ };
51
+
52
+ const productionServices: OrganizationRecoveryRouteServices = {
53
+ requireCanonicalHumanRequestIdentity,
54
+ getManagedAuthRequestActorAdmissionStamp,
55
+ getManagedAuthRequestActorLeaseStamp,
56
+ getOrganizationRecoveryOverview,
57
+ configureOrganizationRecoveryPolicy,
58
+ acceptOrganizationRecoveryCustody,
59
+ disableOrganizationRecoveryPolicy,
60
+ startOrganizationRecoveryOperation,
61
+ approveOrganizationRecoveryOperation,
62
+ cancelOrganizationRecoveryOperation,
63
+ executeOrganizationRecoveryOperation,
64
+ };
65
+
66
+ async function requireRecoveryIdentity(
67
+ context: Context,
68
+ deps: ApiRouteDeps,
69
+ services: OrganizationRecoveryRouteServices,
70
+ ) {
71
+ // Recovery is a browser-session ceremony. Never let ambient API, bearer,
72
+ // service, or machine authority become a competing recovery identity.
73
+ if (
74
+ deps.settings.productAccessMode !== "managed" ||
75
+ !deps.managedAuth ||
76
+ !context.req.header("cookie") ||
77
+ context.req.header("authorization")
78
+ ) {
79
+ throw new HTTPException(401, {
80
+ message: "Managed human authentication required",
81
+ });
82
+ }
83
+ const identity = await services.requireCanonicalHumanRequestIdentity(context, {
84
+ db: deps.db,
85
+ managedAuth: deps.managedAuth,
86
+ managedAuthSessionAdapter: deps.managedAuthSessionAdapter,
87
+ managedAuthSessionSetMode: deps.settings.managedAuthSessionSetMode,
88
+ allowRecovery: false,
89
+ });
90
+ return { ...identity, actorSubjectId: `user:${identity.authUserId}` };
91
+ }
92
+
93
+ async function parseBody<S extends z.ZodType>(context: Context, schema: S): Promise<z.infer<S>> {
94
+ const parsed = schema.safeParse(await context.req.json().catch(() => null));
95
+ if (!parsed.success) {
96
+ throw new HTTPException(422, {
97
+ message: "Invalid organization recovery request",
98
+ });
99
+ }
100
+ return parsed.data;
101
+ }
102
+
103
+ function parseId(schema: z.ZodString, value: string, label: string): string {
104
+ const parsed = schema.safeParse(value);
105
+ if (!parsed.success) throw new HTTPException(422, { message: `Invalid ${label}` });
106
+ return parsed.data;
107
+ }
108
+
109
+ function organizationId(context: Context): string {
110
+ return parseId(OrganizationId, context.req.param("organizationId") ?? "", "organization id");
111
+ }
112
+
113
+ function recoveryOperationId(context: Context): string {
114
+ return parseId(
115
+ RecoveryOperationId,
116
+ context.req.param("recoveryOperationId") ?? "",
117
+ "recovery operation id",
118
+ );
119
+ }
120
+
121
+ export function organizationRecoveryHttpError(error: unknown): Error {
122
+ if (error instanceof OrganizationRecoveryRevisionConflictError) {
123
+ return new ApiHttpError(409, {
124
+ code: "conflict",
125
+ message: "Recovery state changed; refresh before submitting a new action.",
126
+ retryable: false,
127
+ outcomeUnknown: false,
128
+ details: { code: error.code },
129
+ });
130
+ }
131
+ if (error instanceof OrganizationRecoveryOperationReuseError) {
132
+ return new ApiHttpError(409, {
133
+ code: "idempotency_conflict",
134
+ message: "Operation id was reused with different input.",
135
+ retryable: false,
136
+ outcomeUnknown: false,
137
+ details: { code: error.code },
138
+ });
139
+ }
140
+ if (error instanceof OrganizationRecoveryUnavailableError) {
141
+ return new ApiHttpError(409, {
142
+ code: "conflict",
143
+ message: "Organization recovery is unavailable.",
144
+ retryable: false,
145
+ outcomeUnknown: false,
146
+ details: { code: error.code },
147
+ });
148
+ }
149
+ if (error instanceof OrganizationRecoveryDeniedError) {
150
+ return new ApiHttpError(404, {
151
+ code: "not_found",
152
+ message: "Organization recovery not found.",
153
+ retryable: false,
154
+ outcomeUnknown: false,
155
+ });
156
+ }
157
+ if (error instanceof Error) return error;
158
+ return new Error("Organization recovery failed");
159
+ }
160
+
161
+ function requireActorFence(
162
+ context: Context,
163
+ deps: ApiRouteDeps,
164
+ services: OrganizationRecoveryRouteServices,
165
+ ) {
166
+ const actorFence = services.getManagedAuthRequestActorLeaseStamp(context.req.raw);
167
+ if (deps.settings.managedAuthSessionSetMode !== "legacy" && !actorFence) {
168
+ throw new HTTPException(409, {
169
+ message: "Managed actor mutation fence is unavailable",
170
+ });
171
+ }
172
+ if (!actorFence) {
173
+ throw new HTTPException(409, {
174
+ message: "Organization recovery requires provider-neutral browser login slots",
175
+ });
176
+ }
177
+ return actorFence;
178
+ }
179
+
180
+ async function mutationContext(
181
+ context: Context,
182
+ deps: ApiRouteDeps,
183
+ services: OrganizationRecoveryRouteServices,
184
+ ) {
185
+ const identity = await requireRecoveryIdentity(context, deps, services);
186
+ return {
187
+ organizationId: organizationId(context),
188
+ actorSubjectId: identity.actorSubjectId,
189
+ actorAuthUserId: identity.authUserId,
190
+ actorAuthSessionId: identity.authSessionId,
191
+ actorFence: requireActorFence(context, deps, services),
192
+ };
193
+ }
194
+
195
+ function mutationResponse(context: Context, value: unknown): Response {
196
+ return context.json(OrganizationRecoveryMutationResponse.parse(value));
197
+ }
198
+
199
+ export function registerOrganizationRecoveryRoutes(
200
+ app: Hono,
201
+ deps: ApiRouteDeps,
202
+ services: OrganizationRecoveryRouteServices = productionServices,
203
+ ): void {
204
+ const base = "/v1/organizations/:organizationId/recovery";
205
+
206
+ app.get(base, async (context) => {
207
+ const identity = await requireRecoveryIdentity(context, deps, services);
208
+ try {
209
+ return context.json(
210
+ OrganizationRecoveryOverview.parse(
211
+ await services.getOrganizationRecoveryOverview(deps.db, {
212
+ organizationId: organizationId(context),
213
+ actorSubjectId: identity.actorSubjectId,
214
+ actorAuthUserId: identity.authUserId,
215
+ actorAuthSessionId: identity.authSessionId,
216
+ actorFence: services.getManagedAuthRequestActorAdmissionStamp(context.req.raw) ?? null,
217
+ }),
218
+ ),
219
+ );
220
+ } catch (error) {
221
+ throw organizationRecoveryHttpError(error);
222
+ }
223
+ });
224
+
225
+ app.put(`${base}/policy`, async (context) => {
226
+ const authority = await mutationContext(context, deps, services);
227
+ const payload = await parseBody(context, ConfigureOrganizationRecoveryPolicyRequest);
228
+ try {
229
+ return mutationResponse(
230
+ context,
231
+ await services.configureOrganizationRecoveryPolicy(deps.db, {
232
+ ...authority,
233
+ operationId: payload.operationId,
234
+ expectedPolicyRevision: payload.expectedPolicyRevision,
235
+ custodianMembershipIds: payload.custodianMembershipIds,
236
+ }),
237
+ );
238
+ } catch (error) {
239
+ throw organizationRecoveryHttpError(error);
240
+ }
241
+ });
242
+
243
+ app.post(`${base}/policy/accept`, async (context) => {
244
+ const authority = await mutationContext(context, deps, services);
245
+ const payload = await parseBody(context, AcceptOrganizationRecoveryCustodyRequest);
246
+ try {
247
+ return mutationResponse(
248
+ context,
249
+ await services.acceptOrganizationRecoveryCustody(deps.db, {
250
+ ...authority,
251
+ operationId: payload.operationId,
252
+ expectedPolicyRevision: payload.expectedPolicyRevision,
253
+ }),
254
+ );
255
+ } catch (error) {
256
+ throw organizationRecoveryHttpError(error);
257
+ }
258
+ });
259
+
260
+ app.post(`${base}/policy/disable`, async (context) => {
261
+ const authority = await mutationContext(context, deps, services);
262
+ const payload = await parseBody(context, DisableOrganizationRecoveryPolicyRequest);
263
+ try {
264
+ return mutationResponse(
265
+ context,
266
+ await services.disableOrganizationRecoveryPolicy(deps.db, {
267
+ ...authority,
268
+ operationId: payload.operationId,
269
+ expectedPolicyRevision: payload.expectedPolicyRevision,
270
+ }),
271
+ );
272
+ } catch (error) {
273
+ throw organizationRecoveryHttpError(error);
274
+ }
275
+ });
276
+
277
+ app.post(`${base}/operations`, async (context) => {
278
+ const authority = await mutationContext(context, deps, services);
279
+ const payload = await parseBody(context, StartOrganizationRecoveryOperationRequest);
280
+ try {
281
+ return mutationResponse(
282
+ context,
283
+ await services.startOrganizationRecoveryOperation(deps.db, {
284
+ ...authority,
285
+ operationId: payload.operationId,
286
+ expectedPolicyRevision: payload.expectedPolicyRevision,
287
+ targetMembershipId: payload.targetMembershipId,
288
+ }),
289
+ );
290
+ } catch (error) {
291
+ throw organizationRecoveryHttpError(error);
292
+ }
293
+ });
294
+
295
+ async function mutateOperation(
296
+ context: Context,
297
+ command: typeof approveOrganizationRecoveryOperation,
298
+ ): Promise<Response> {
299
+ const authority = await mutationContext(context, deps, services);
300
+ const payload = await parseBody(context, OrganizationRecoveryOperationCommandRequest);
301
+ try {
302
+ return mutationResponse(
303
+ context,
304
+ await command(deps.db, {
305
+ ...authority,
306
+ recoveryOperationId: recoveryOperationId(context),
307
+ operationId: payload.operationId,
308
+ expectedOperationRevision: payload.expectedOperationRevision,
309
+ }),
310
+ );
311
+ } catch (error) {
312
+ throw organizationRecoveryHttpError(error);
313
+ }
314
+ }
315
+
316
+ app.post(`${base}/operations/:recoveryOperationId/approve`, async (context) =>
317
+ mutateOperation(context, services.approveOrganizationRecoveryOperation),
318
+ );
319
+ app.post(`${base}/operations/:recoveryOperationId/cancel`, async (context) =>
320
+ mutateOperation(context, services.cancelOrganizationRecoveryOperation),
321
+ );
322
+ app.post(`${base}/operations/:recoveryOperationId/execute`, async (context) =>
323
+ mutateOperation(context, services.executeOrganizationRecoveryOperation),
324
+ );
325
+ }