@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
@@ -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
  },