@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
package/src/mcp/server.ts CHANGED
@@ -186,7 +186,7 @@ import {
186
186
  import {
187
187
  acceptSessionUserMessageWithOutcome,
188
188
  controlAgentSessionWorkstream,
189
- controlHumanSessionWorkstream,
189
+ controlHumanSessionWorkstreamWithOutcome,
190
190
  createSessionForRequestWithOutcome,
191
191
  SessionSpawnDeniedError,
192
192
  sessionSpawnDenialEnvelope,
@@ -223,7 +223,11 @@ import {
223
223
  SESSION_WAIT_MAX_TARGETS,
224
224
  waitForSessionChanges,
225
225
  } from "./session-wait";
226
- import { mcpMutationReceipt, sessionCreateMutationReceipt } from "./receipts";
226
+ import {
227
+ mcpMutationReceipt,
228
+ sessionControlMutationReceipt,
229
+ sessionCreateMutationReceipt,
230
+ } from "./receipts";
227
231
  import {
228
232
  boundScheduledTaskDetailMcp,
229
233
  boundScheduledTaskMcpPage,
@@ -4537,27 +4541,18 @@ function registerWorkspaceOrchestrationTools(
4537
4541
  controlled.authorization?.relatedSessionAccess ?? "root",
4538
4542
  );
4539
4543
  return json(
4540
- mcpMutationReceipt({
4544
+ sessionControlMutationReceipt({
4541
4545
  operation: "session_pause",
4542
- committed: true,
4543
- outcome: controlled.replay ? "replayed" : "updated",
4544
- changed: !controlled.replay,
4545
- resource: {
4546
- type: "session",
4547
- id: sessionId,
4548
- state: effectiveControl.state,
4549
- },
4550
- relatedResources: [{ type: "session_command_receipt", id: controlled.receipt.id }],
4546
+ sessionId,
4547
+ state: effectiveControl.state,
4548
+ receiptId: controlled.receipt.id,
4551
4549
  timestamp: controlled.receipt.createdAt.toISOString(),
4552
- idempotency: {
4553
- status: controlled.replay ? "replayed" : "applied",
4554
- },
4555
- facts: { interruptionCount: controlled.interruptionCount },
4556
- nextAction: { tool: "session_get", arguments: { sessionId } },
4550
+ outcome: controlled.outcome,
4551
+ interruptionCount: controlled.interruptionCount,
4557
4552
  }),
4558
4553
  );
4559
4554
  }
4560
- const controlled = await controlHumanSessionWorkstream(
4555
+ const controlled = await controlHumanSessionWorkstreamWithOutcome(
4561
4556
  deps,
4562
4557
  {
4563
4558
  accountId: grant.accountId,
@@ -4572,7 +4567,17 @@ function registerWorkspaceOrchestrationTools(
4572
4567
  ...(reason ? { reason } : {}),
4573
4568
  },
4574
4569
  );
4575
- return json(controlled);
4570
+ return json(
4571
+ sessionControlMutationReceipt({
4572
+ operation: "session_pause",
4573
+ sessionId,
4574
+ state: controlled.response.effectiveControl.state,
4575
+ receiptId: controlled.response.receipt.id,
4576
+ timestamp: controlled.response.receipt.createdAt,
4577
+ outcome: controlled.outcome,
4578
+ interruptionCount: controlled.response.interruptionCount,
4579
+ }),
4580
+ );
4576
4581
  },
4577
4582
  );
4578
4583
 
@@ -4605,27 +4610,18 @@ function registerWorkspaceOrchestrationTools(
4605
4610
  controlled.authorization?.relatedSessionAccess ?? "root",
4606
4611
  );
4607
4612
  return json(
4608
- mcpMutationReceipt({
4613
+ sessionControlMutationReceipt({
4609
4614
  operation: "session_resume",
4610
- committed: true,
4611
- outcome: controlled.replay ? "replayed" : "updated",
4612
- changed: !controlled.replay,
4613
- resource: {
4614
- type: "session",
4615
- id: sessionId,
4616
- state: effectiveControl.state,
4617
- },
4618
- relatedResources: [{ type: "session_command_receipt", id: controlled.receipt.id }],
4615
+ sessionId,
4616
+ state: effectiveControl.state,
4617
+ receiptId: controlled.receipt.id,
4619
4618
  timestamp: controlled.receipt.createdAt.toISOString(),
4620
- idempotency: {
4621
- status: controlled.replay ? "replayed" : "applied",
4622
- },
4623
- facts: { interruptionCount: controlled.interruptionCount },
4624
- nextAction: { tool: "session_get", arguments: { sessionId } },
4619
+ outcome: controlled.outcome,
4620
+ interruptionCount: controlled.interruptionCount,
4625
4621
  }),
4626
4622
  );
4627
4623
  }
4628
- const controlled = await controlHumanSessionWorkstream(
4624
+ const controlled = await controlHumanSessionWorkstreamWithOutcome(
4629
4625
  deps,
4630
4626
  {
4631
4627
  accountId: grant.accountId,
@@ -4640,7 +4636,17 @@ function registerWorkspaceOrchestrationTools(
4640
4636
  ...(reason ? { reason } : {}),
4641
4637
  },
4642
4638
  );
4643
- return json(controlled);
4639
+ return json(
4640
+ sessionControlMutationReceipt({
4641
+ operation: "session_resume",
4642
+ sessionId,
4643
+ state: controlled.response.effectiveControl.state,
4644
+ receiptId: controlled.response.receipt.id,
4645
+ timestamp: controlled.response.receipt.createdAt,
4646
+ outcome: controlled.outcome,
4647
+ interruptionCount: controlled.response.interruptionCount,
4648
+ }),
4649
+ );
4644
4650
  },
4645
4651
  );
4646
4652
 
@@ -4741,13 +4747,13 @@ function registerWorkspaceOrchestrationTools(
4741
4747
  });
4742
4748
  }
4743
4749
  if (accepted.action === "conflict") {
4744
- throw new Error(`human-input request is ${accepted.request.status}`);
4750
+ throw new Error("human-input request is not currently actionable");
4745
4751
  }
4746
4752
  return json(
4747
4753
  mcpMutationReceipt({
4748
4754
  operation: "session_human_input_respond",
4749
4755
  committed: true,
4750
- outcome: accepted.events.length === 0 ? "replayed" : "updated",
4756
+ outcome: accepted.action === "completed" ? "replayed" : "updated",
4751
4757
  changed: accepted.events.length > 0,
4752
4758
  resource: {
4753
4759
  type: "session_human_input_request",
@@ -4757,7 +4763,7 @@ function registerWorkspaceOrchestrationTools(
4757
4763
  relatedResources: [{ type: "session", id: sessionId }],
4758
4764
  timestamp: accepted.request.respondedAt ?? new Date().toISOString(),
4759
4765
  idempotency: {
4760
- status: accepted.events.length === 0 ? "replayed" : "applied",
4766
+ status: accepted.action === "completed" ? "replayed" : "applied",
4761
4767
  },
4762
4768
  facts: { outcome: accepted.request.response?.outcome ?? null },
4763
4769
  nextAction: { tool: "session_get", arguments: { sessionId } },
@@ -0,0 +1,103 @@
1
+ import type { Database } from "@opengeni/db";
2
+ import {
3
+ prepareOrganizationRecoveryNotifications,
4
+ settleOrganizationRecoveryNotification,
5
+ type OrganizationRecoveryNotificationClaim,
6
+ type OrganizationRecoveryNotificationSettlement,
7
+ } from "@opengeni/db";
8
+
9
+ export type OrganizationRecoveryNotificationDeliveryResult =
10
+ | { status: "sent"; providerMessageId: string | null }
11
+ | { status: "failed"; errorClass: string }
12
+ | { status: "outcome_unknown" };
13
+
14
+ export interface OrganizationRecoveryNotificationTransport {
15
+ readonly provider: string;
16
+ send(
17
+ claim: OrganizationRecoveryNotificationClaim,
18
+ ): Promise<OrganizationRecoveryNotificationDeliveryResult>;
19
+ }
20
+
21
+ export type OrganizationRecoveryNotificationLifecycle = {
22
+ prepare: typeof prepareOrganizationRecoveryNotifications;
23
+ settle: typeof settleOrganizationRecoveryNotification;
24
+ };
25
+
26
+ const productionLifecycle: OrganizationRecoveryNotificationLifecycle = {
27
+ prepare: prepareOrganizationRecoveryNotifications,
28
+ settle: settleOrganizationRecoveryNotification,
29
+ };
30
+
31
+ export async function dispatchOrganizationRecoveryNotifications(input: {
32
+ db: Database;
33
+ transport: OrganizationRecoveryNotificationTransport;
34
+ claimOwner: string;
35
+ limit?: number;
36
+ leaseSeconds?: number;
37
+ lifecycle?: OrganizationRecoveryNotificationLifecycle;
38
+ }): Promise<OrganizationRecoveryNotificationSettlement[]> {
39
+ const lifecycle = input.lifecycle ?? productionLifecycle;
40
+ const claims = await lifecycle.prepare(input.db, {
41
+ provider: input.transport.provider,
42
+ claimOwner: input.claimOwner,
43
+ limit: input.limit ?? 25,
44
+ leaseSeconds: input.leaseSeconds ?? 60,
45
+ });
46
+ return await Promise.all(
47
+ claims.map(async (claim) => {
48
+ let result: OrganizationRecoveryNotificationDeliveryResult;
49
+ try {
50
+ result = await input.transport.send(claim);
51
+ } catch {
52
+ // The provider may have accepted the stable idempotency key before the
53
+ // transport failed. Preserve ambiguity for explicit reconciliation.
54
+ result = { status: "outcome_unknown" };
55
+ }
56
+ return await lifecycle.settle(input.db, {
57
+ outboxId: claim.outboxId,
58
+ deliveryId: claim.deliveryId,
59
+ claimOwner: claim.claimOwner,
60
+ phase: result.status,
61
+ ...(result.status === "sent" ? { providerMessageId: result.providerMessageId } : {}),
62
+ ...(result.status === "failed" ? { errorClass: result.errorClass } : {}),
63
+ });
64
+ }),
65
+ );
66
+ }
67
+
68
+ export class InMemoryOrganizationRecoveryNotificationTransport implements OrganizationRecoveryNotificationTransport {
69
+ readonly provider = "fake";
70
+ readonly attempts: Array<{
71
+ idempotencyKey: string;
72
+ payloadDigest: string;
73
+ recipientCanonicalIdentityId: string;
74
+ providerMessageId: string;
75
+ }> = [];
76
+ private readonly deliveries = new Map<string, string>();
77
+ private readonly scripted: OrganizationRecoveryNotificationDeliveryResult[] = [];
78
+
79
+ enqueue(...results: OrganizationRecoveryNotificationDeliveryResult[]): void {
80
+ this.scripted.push(...results);
81
+ }
82
+
83
+ logicalDeliveryCount(): number {
84
+ return this.deliveries.size;
85
+ }
86
+
87
+ async send(
88
+ claim: OrganizationRecoveryNotificationClaim,
89
+ ): Promise<OrganizationRecoveryNotificationDeliveryResult> {
90
+ const scripted = this.scripted.shift();
91
+ if (scripted) return scripted;
92
+ const providerMessageId =
93
+ this.deliveries.get(claim.idempotencyKey) ?? `fake:${crypto.randomUUID()}`;
94
+ this.deliveries.set(claim.idempotencyKey, providerMessageId);
95
+ this.attempts.push({
96
+ idempotencyKey: claim.idempotencyKey,
97
+ payloadDigest: claim.payloadDigest,
98
+ recipientCanonicalIdentityId: claim.recipientCanonicalIdentityId,
99
+ providerMessageId,
100
+ });
101
+ return { status: "sent", providerMessageId };
102
+ }
103
+ }
@@ -5,7 +5,11 @@ import {
5
5
  CanonicalHumanIdentityProjection,
6
6
  LinkCanonicalHumanLoginBindingRequest,
7
7
  } from "@opengeni/contracts/canonical-human-identities";
8
- import { requireCanonicalHumanRequestIdentity } from "@opengeni/core/canonical-human-identities";
8
+ import {
9
+ getManagedAuthRequestActorLeaseStamp,
10
+ markManagedAuthRequestActorTransitionApplied,
11
+ requireCanonicalHumanRequestIdentity,
12
+ } from "@opengeni/core/canonical-human-identities";
9
13
  import type { ApiRouteDeps } from "@opengeni/core";
10
14
  import {
11
15
  applyCanonicalHumanIdentityOperation,
@@ -13,6 +17,7 @@ import {
13
17
  CanonicalHumanIdentityConflictError,
14
18
  CanonicalHumanIdentityNotFoundError,
15
19
  CanonicalHumanIdentityOperationReuseError,
20
+ CanonicalHumanIdentityMutationInFlightError,
16
21
  getCanonicalHumanIdentityProjection,
17
22
  } from "@opengeni/db/canonical-human-identities";
18
23
  import type { Context, Hono } from "hono";
@@ -25,6 +30,8 @@ async function requestIdentity(context: Context, deps: ApiRouteDeps) {
25
30
  return await requireCanonicalHumanRequestIdentity(context, {
26
31
  db: deps.db,
27
32
  ...(deps.managedAuth === undefined ? {} : { managedAuth: deps.managedAuth }),
33
+ managedAuthSessionAdapter: deps.managedAuthSessionAdapter,
34
+ managedAuthSessionSetMode: deps.settings.managedAuthSessionSetMode,
28
35
  allowRecovery: true,
29
36
  });
30
37
  }
@@ -50,6 +57,9 @@ function identityError(context: Context, error: unknown): Response {
50
57
  if (error instanceof CanonicalHumanIdentityOperationReuseError) {
51
58
  return context.json({ code: error.code, message: error.message }, 409);
52
59
  }
60
+ if (error instanceof CanonicalHumanIdentityMutationInFlightError) {
61
+ return context.json({ code: error.code, message: error.message, retryable: true }, 409);
62
+ }
53
63
  if (error instanceof CanonicalHumanIdentityNotFoundError) {
54
64
  return context.json(
55
65
  { code: "CANONICAL_HUMAN_IDENTITY_NOT_FOUND", message: error.message },
@@ -79,21 +89,26 @@ async function mutate(
79
89
  },
80
90
  ): Promise<Response> {
81
91
  const identity = await requestIdentity(context, deps);
92
+ const actorFence = getManagedAuthRequestActorLeaseStamp(context.req.raw);
93
+ if (deps.settings.managedAuthSessionSetMode !== "legacy" && !actorFence) {
94
+ throw new HTTPException(409, { message: "Managed actor mutation fence is unavailable" });
95
+ }
82
96
  try {
83
- return context.json(
84
- CanonicalHumanIdentityMutationResponse.parse(
85
- await applyCanonicalHumanIdentityOperation(deps.db, {
86
- operationId: input.operationId,
87
- authUserId: identity.authUserId,
88
- expectedIdentityRevision: input.expectedIdentityRevision,
89
- operationType: input.operationType,
90
- ...(input.bindingId ? { bindingId: input.bindingId } : {}),
91
- ...(input.providerId ? { providerId: input.providerId } : {}),
92
- ...(input.providerAccountId ? { providerAccountId: input.providerAccountId } : {}),
93
- reason: input.reason,
94
- }),
95
- ),
97
+ const response = CanonicalHumanIdentityMutationResponse.parse(
98
+ await applyCanonicalHumanIdentityOperation(deps.db, {
99
+ operationId: input.operationId,
100
+ authUserId: identity.authUserId,
101
+ expectedIdentityRevision: input.expectedIdentityRevision,
102
+ operationType: input.operationType,
103
+ ...(input.bindingId ? { bindingId: input.bindingId } : {}),
104
+ ...(input.providerId ? { providerId: input.providerId } : {}),
105
+ ...(input.providerAccountId ? { providerAccountId: input.providerAccountId } : {}),
106
+ reason: input.reason,
107
+ ...(actorFence ? { actorFence } : {}),
108
+ }),
96
109
  );
110
+ if (actorFence) markManagedAuthRequestActorTransitionApplied(context.req.raw);
111
+ return context.json(response);
97
112
  } catch (error) {
98
113
  return identityError(context, error);
99
114
  }
@@ -195,7 +195,11 @@ async function managedCookieHuman(
195
195
  ) {
196
196
  return null;
197
197
  }
198
- const session = await getManagedSession(c, deps.managedAuth);
198
+ const session = await getManagedSession(c, deps.managedAuth, {
199
+ db: deps.db,
200
+ sessionAdapter: deps.managedAuthSessionAdapter,
201
+ sessionSetMode: deps.settings.managedAuthSessionSetMode,
202
+ });
199
203
  if (!session?.user?.id || !session.session?.id) return null;
200
204
  return {
201
205
  subjectId: `user:${session.user.id}`,
@@ -1,5 +1,7 @@
1
1
  import {
2
2
  CreateVariableSetRequest,
3
+ ResolveVariableSetAttachmentsRequest,
4
+ ResolveVariableSetAttachmentsResponse,
3
5
  SetVariableSetVariableRequest,
4
6
  UpdateVariableSetRequest,
5
7
  VariableSetVariableName,
@@ -14,6 +16,7 @@ import {
14
16
  getVariableSetByName,
15
17
  listVariableSets,
16
18
  readVariableSetSecretAtomically,
19
+ resolveVariableSetAttachments,
17
20
  setVariableSetVariable,
18
21
  updateVariableSet,
19
22
  VariableSetAttachedError,
@@ -135,6 +138,26 @@ export function registerVariableSetRoutes(app: Hono, deps: ApiRouteDeps): void {
135
138
  return c.json(created, 201);
136
139
  });
137
140
 
141
+ if (prefix.endsWith("/variable-sets")) {
142
+ app.post(`${prefix}/resolve-attachments`, async (c) => {
143
+ const workspaceId = c.req.param("workspaceId")!;
144
+ const grant = await requireAccessGrant(c, deps, workspaceId);
145
+ requirePermission(grant, "variable-sets:attach");
146
+ requirePermission(grant, "variable-sets:use");
147
+ const payload = ResolveVariableSetAttachmentsRequest.parse(await c.req.json());
148
+ const variableSets = await resolveVariableSetAttachments(
149
+ db,
150
+ {
151
+ accountId: grant.accountId,
152
+ workspaceId,
153
+ subjectId: grant.subjectId,
154
+ },
155
+ payload.variableSetIds,
156
+ );
157
+ return c.json(ResolveVariableSetAttachmentsResponse.parse({ variableSets }));
158
+ });
159
+ }
160
+
138
161
  app.get(`${prefix}/:variableSetId`, async (c) => {
139
162
  const workspaceId = c.req.param("workspaceId")!;
140
163
  const grant = await requireAccessGrant(c, deps, workspaceId);
@@ -45,6 +45,7 @@ import {
45
45
  updateSiteAuthConnection,
46
46
  } from "@opengeni/db";
47
47
  import {
48
+ getManagedAuthRequestActorEpoch,
48
49
  SessionAuthorizationDeniedError,
49
50
  SessionAuthorizationUnavailableError,
50
51
  requireAccessGrant,
@@ -74,6 +75,7 @@ export function registerInteractionResourceRoutes(app: Hono, deps: ApiRouteDeps)
74
75
  context.req.raw.signal,
75
76
  {
76
77
  observability: deps.observability,
78
+ actorEpoch: getManagedAuthRequestActorEpoch(context.req.raw) ?? undefined,
77
79
  reauthorize: async () => {
78
80
  const freshGrant = await requireFreshAccessGrant(
79
81
  context,
@@ -98,6 +100,7 @@ export function registerInteractionResourceRoutes(app: Hono, deps: ApiRouteDeps)
98
100
  context.req.raw.signal,
99
101
  {
100
102
  observability: deps.observability,
103
+ actorEpoch: getManagedAuthRequestActorEpoch(context.req.raw) ?? undefined,
101
104
  reauthorize: async () => {
102
105
  await requireFreshAccessGrant(context, deps, workspaceId, "sessions:read");
103
106
  },