@opengeni/api-router 2.3.2-canary.2 → 2.4.2-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 (39) 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-L7GVVSSQ.js} +2996 -373
  5. package/dist/chunk-L7GVVSSQ.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/mcp/receipts.d.ts +9 -0
  12. package/dist/organization-recovery-notifications.d.ts +41 -0
  13. package/dist/routes/managed-auth-session-sets.d.ts +19 -0
  14. package/dist/routes/organization-recovery.d.ts +19 -0
  15. package/dist/routes/workspaces.d.ts +1 -0
  16. package/package.json +18 -18
  17. package/src/app.ts +133 -1
  18. package/src/auth/managed-auth-attempt-context.ts +24 -0
  19. package/src/auth/managed-auth-session-adapter.ts +205 -0
  20. package/src/auth/managed-auth.ts +52 -2
  21. package/src/fatal-process-boundary.ts +231 -0
  22. package/src/http/sse.ts +7 -0
  23. package/src/index.ts +25 -5
  24. package/src/integrations/slack-interactions.ts +30 -17
  25. package/src/mcp/receipts.ts +34 -0
  26. package/src/mcp/server.ts +45 -39
  27. package/src/organization-recovery-notifications.ts +103 -0
  28. package/src/routes/canonical-human-identities.ts +29 -14
  29. package/src/routes/codex.ts +5 -1
  30. package/src/routes/environments.ts +23 -0
  31. package/src/routes/interaction-resources.ts +3 -0
  32. package/src/routes/managed-auth-session-sets.ts +994 -0
  33. package/src/routes/managed-onboarding.ts +2 -0
  34. package/src/routes/organization-memberships.ts +2 -0
  35. package/src/routes/organization-recovery.ts +325 -0
  36. package/src/routes/sessions.ts +24 -39
  37. package/src/routes/supergrok.ts +5 -1
  38. package/src/routes/workspaces.ts +23 -1
  39. 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
+ }
@@ -211,6 +211,7 @@ import type { Context, Hono, MiddlewareHandler } from "hono";
211
211
  import { HTTPException } from "hono/http-exception";
212
212
  import type { ContentfulStatusCode } from "hono/utils/http-status";
213
213
  import {
214
+ getManagedAuthRequestActorEpoch,
214
215
  hasPermission,
215
216
  requireAccessGrant,
216
217
  requireAccessGrantAuthorization,
@@ -243,7 +244,6 @@ import {
243
244
  import { buildSessionCodexRealtimeBroker, CodexRealtimeBrokerError } from "../codex-realtime";
244
245
  import {
245
246
  acceptSessionUserMessage,
246
- acceptSessionUserMessageWithOutcome,
247
247
  controlHumanSessionWorkstream,
248
248
  createSessionForRequest,
249
249
  deleteHumanQueuePrompt,
@@ -259,6 +259,7 @@ import {
259
259
  SessionSpawnDeniedError,
260
260
  sessionSpawnDenialEnvelope,
261
261
  steerHumanQueuePrompt,
262
+ submitComposerDraftForRequest,
262
263
  updateSessionMcpApprovalPolicy,
263
264
  updateManagedHumanSessionVisibility,
264
265
  updateSessionToolPolicy,
@@ -1572,7 +1573,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1572
1573
  // while the actively-working label remains independent of acknowledgment.
1573
1574
  app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/attention", async (c) => {
1574
1575
  const workspaceId = c.req.param("workspaceId");
1575
- const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
1576
+ const authorization = await requireAccessGrantAuthorization(
1577
+ c,
1578
+ deps,
1579
+ workspaceId,
1580
+ "sessions:read",
1581
+ );
1582
+ const grant = authorization.grant;
1576
1583
  const sessionId = c.req.param("sessionId");
1577
1584
  if (!z.string().uuid().safeParse(sessionId).success) {
1578
1585
  throw new HTTPException(404, { message: "session not found" });
@@ -1586,6 +1593,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1586
1593
  workspaceId,
1587
1594
  subjectId: grant.subjectId,
1588
1595
  sessionId,
1596
+ personalWorkspaceOwnerException: authorization.canonicalManagedHumanSession,
1589
1597
  ...parsed.data,
1590
1598
  });
1591
1599
  if (!session) throw new HTTPException(404, { message: "session not found" });
@@ -1618,7 +1626,13 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1618
1626
  // for this member and remain recoverable through the archived list view.
1619
1627
  app.put("/v1/workspaces/:workspaceId/sessions/:sessionId/archive", async (c) => {
1620
1628
  const workspaceId = c.req.param("workspaceId");
1621
- const grant = await requireAccessGrant(c, deps, workspaceId, "sessions:read");
1629
+ const authorization = await requireAccessGrantAuthorization(
1630
+ c,
1631
+ deps,
1632
+ workspaceId,
1633
+ "sessions:read",
1634
+ );
1635
+ const grant = authorization.grant;
1622
1636
  const sessionId = c.req.param("sessionId");
1623
1637
  const parsed = UpdateSessionArchiveRequest.safeParse(await c.req.json().catch(() => null));
1624
1638
  if (!parsed.success) {
@@ -1629,6 +1643,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
1629
1643
  workspaceId,
1630
1644
  subjectId: grant.subjectId,
1631
1645
  sessionId,
1646
+ personalWorkspaceOwnerException: authorization.canonicalManagedHumanSession,
1632
1647
  ...parsed.data,
1633
1648
  });
1634
1649
  if (!session) throw new HTTPException(404, { message: "session not found" });
@@ -2559,6 +2574,7 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2559
2574
  c.req.raw.signal,
2560
2575
  {
2561
2576
  observability: deps.observability,
2577
+ actorEpoch: getManagedAuthRequestActorEpoch(c.req.raw) ?? undefined,
2562
2578
  reauthorizeAfterMs:
2563
2579
  authorization?.reauthorizeAfterMs ?? SESSION_AUTHORIZATION_DEFAULT_REAUTHORIZE_MS,
2564
2580
  reauthorize: async () => {
@@ -2832,46 +2848,15 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2832
2848
  const sessionId = c.req.param("sessionId");
2833
2849
  await assertSessionExists(db, workspaceId, sessionId);
2834
2850
  const payload = SubmitComposerDraftRequest.parse(await c.req.json().catch(() => null));
2835
- let result: Awaited<ReturnType<typeof acceptSessionUserMessageWithOutcome>>;
2851
+ let result: Awaited<ReturnType<typeof submitComposerDraftForRequest>>;
2836
2852
  try {
2837
- result = await acceptSessionUserMessageWithOutcome(deps, grant, workspaceId, sessionId, {
2838
- text: payload.text,
2839
- annotations: payload.annotations,
2840
- modelContext: payload.modelContext ?? null,
2841
- resources: payload.resources,
2842
- model: payload.model,
2843
- reasoningEffort: payload.reasoningEffort,
2844
- latencyMode: payload.latencyMode,
2845
- mcpCredentialUpdates: payload.mcpCredentialUpdates ?? [],
2846
- connectionAuthorities: payload.connectionAuthorities,
2847
- ...(payload.personalResourceAttachment
2848
- ? { personalResourceAttachment: payload.personalResourceAttachment }
2849
- : {}),
2853
+ result = await submitComposerDraftForRequest(deps, grant, workspaceId, sessionId, payload, {
2850
2854
  authorization,
2851
- delivery: payload.delivery,
2852
- origin: "human",
2853
- expectedDraftRevision: payload.expectedDraftRevision,
2854
- clientEventId: payload.clientEventId,
2855
- ...(payload.controlEtag ? { controlEtag: payload.controlEtag } : {}),
2856
2855
  });
2857
2856
  } catch (error) {
2858
2857
  return commandConflictResponse(c, error);
2859
2858
  }
2860
- if (!result.draft) {
2861
- throw new Error("Accepted composer draft submission did not return its next draft");
2862
- }
2863
- return c.json(
2864
- {
2865
- accepted: result.accepted,
2866
- turn: result.turn,
2867
- draft: result.draft,
2868
- receipt: result.receipt,
2869
- routing: result.routing,
2870
- interruptionCount: result.interruptionCount,
2871
- replay: result.replay,
2872
- },
2873
- 202,
2874
- );
2859
+ return c.json(result, 202);
2875
2860
  });
2876
2861
 
2877
2862
  app.post("/v1/workspaces/:workspaceId/sessions/:sessionId/events", async (c) => {
@@ -2995,10 +2980,10 @@ export function registerSessionRoutes(app: Hono, deps: SessionRouteDeps): void {
2995
2980
  }
2996
2981
  if (accepted.action === "conflict") {
2997
2982
  throw new HTTPException(409, {
2998
- message: `human-input request is ${accepted.request.status}`,
2983
+ message: "human-input request is not currently actionable",
2999
2984
  });
3000
2985
  }
3001
- return c.json(accepted.event, 202);
2986
+ return c.json(accepted.event, accepted.action === "completed" ? 200 : 202);
3002
2987
  }
3003
2988
  });
3004
2989
 
@@ -80,7 +80,11 @@ async function managedCookieHuman(
80
80
  ) {
81
81
  return null;
82
82
  }
83
- const session = await getManagedSession(c, deps.managedAuth);
83
+ const session = await getManagedSession(c, deps.managedAuth, {
84
+ db: deps.db,
85
+ sessionAdapter: deps.managedAuthSessionAdapter,
86
+ sessionSetMode: deps.settings.managedAuthSessionSetMode,
87
+ });
84
88
  return session?.user?.id ? { subjectId: `user:${session.user.id}` } : null;
85
89
  }
86
90
 
@@ -45,6 +45,7 @@ import { boundWorkspaceControlHttpPage } from "@opengeni/events";
45
45
  import type { Hono } from "hono";
46
46
  import { HTTPException } from "hono/http-exception";
47
47
  import {
48
+ getManagedAuthRequestActorEpoch,
48
49
  hasPermission,
49
50
  requireAccessContext,
50
51
  requireAccessGrant,
@@ -58,6 +59,7 @@ import {
58
59
  resolveMemberSubjectId,
59
60
  } from "@opengeni/core";
60
61
  import { boundedLimit } from "../http/common";
62
+ import { ApiHttpError } from "../http/api-error";
61
63
  import { sseWorkspaceControlStream } from "../http/sse";
62
64
  import { buildWorkspaceModelCatalog } from "../model-catalog";
63
65
  import { processTemporalScheduleCleanupClaims } from "../temporal-schedule-cleanup";
@@ -97,6 +99,14 @@ export function workspaceMembersResponse(members: readonly WorkspaceMemberProjec
97
99
  });
98
100
  }
99
101
 
102
+ export function workspaceUpdateRequestsAccountTransfer(value: unknown): boolean {
103
+ return (
104
+ typeof value === "object" &&
105
+ value !== null &&
106
+ Object.prototype.hasOwnProperty.call(value, "accountId")
107
+ );
108
+ }
109
+
100
110
  export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
101
111
  app.get("/v1/access/me", async (c) => {
102
112
  return c.json(await requireAccessContext(c, deps));
@@ -163,7 +173,18 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
163
173
  app.patch("/v1/workspaces/:workspaceId", async (c) => {
164
174
  const workspaceId = c.req.param("workspaceId");
165
175
  await requireAccessGrant(c, deps, workspaceId, "workspace:admin");
166
- const payload = UpdateWorkspaceRequest.parse(await c.req.json());
176
+ const body = await c.req.json();
177
+ if (workspaceUpdateRequestsAccountTransfer(body)) {
178
+ throw new ApiHttpError(409, {
179
+ code: "conflict",
180
+ message:
181
+ "A workspace is permanently owned by one organization; use workspace grants for same-organization access handoff.",
182
+ retryable: false,
183
+ outcomeUnknown: false,
184
+ details: { code: "workspace_transfer_unsupported" },
185
+ });
186
+ }
187
+ const payload = UpdateWorkspaceRequest.parse(body);
167
188
  const workspace = await updateWorkspace(deps.db, workspaceId, {
168
189
  ...(payload.name !== undefined ? { name: payload.name.trim() } : {}),
169
190
  ...(payload.slug !== undefined ? { slug: payload.slug?.trim() || null } : {}),
@@ -363,6 +384,7 @@ export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void {
363
384
  c.req.raw.signal,
364
385
  {
365
386
  observability: deps.observability,
387
+ actorEpoch: getManagedAuthRequestActorEpoch(c.req.raw) ?? undefined,
366
388
  reauthorize: async () => {
367
389
  await requireFreshAccessGrant(c, deps, workspaceId, "workspace:read");
368
390
  },