@opengeni/api-router 2.2.0-canary.0 → 2.3.2-canary.1

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.d.ts +5 -2
  3. package/dist/auth/managed-email.d.ts +29 -0
  4. package/dist/auth/organization-user-setup.d.ts +57 -0
  5. package/dist/{chunk-3TP54PPX.js → chunk-IBV7Z6F4.js} +3545 -2009
  6. package/dist/chunk-IBV7Z6F4.js.map +1 -0
  7. package/dist/index.js +1 -1
  8. package/dist/integrations/slack-app-home.d.ts +1 -1
  9. package/dist/mcp/server.d.ts +1 -1
  10. package/dist/mcp/session-view.d.ts +1 -0
  11. package/dist/routes/automations.d.ts +13 -0
  12. package/dist/routes/insights.d.ts +2 -1
  13. package/dist/routes/managed-onboarding.d.ts +29 -0
  14. package/dist/routes/pr-review-github.d.ts +3 -0
  15. package/package.json +18 -18
  16. package/src/app.ts +56 -5
  17. package/src/auth/managed-auth.ts +29 -34
  18. package/src/auth/managed-email.ts +174 -0
  19. package/src/auth/organization-user-setup.ts +217 -0
  20. package/src/http/auth.ts +15 -0
  21. package/src/http/sse.ts +62 -13
  22. package/src/integrations/slack-app-home.ts +2 -2
  23. package/src/mcp/company-brain-governed-writes.ts +4 -4
  24. package/src/mcp/company-profile-agent-admin.ts +11 -18
  25. package/src/mcp/remember.ts +4 -4
  26. package/src/mcp/server.ts +50 -4
  27. package/src/mcp/session-view.ts +8 -2
  28. package/src/routes/automations.ts +3 -3
  29. package/src/routes/documents.ts +2 -0
  30. package/src/routes/insights.ts +61 -19
  31. package/src/routes/managed-onboarding.ts +317 -0
  32. package/src/routes/organization-memberships.ts +212 -155
  33. package/src/routes/pr-review-github.ts +844 -0
  34. package/src/routes/pr-review.ts +20 -0
  35. package/src/routes/rigs.ts +37 -4
  36. package/src/routes/sessions.ts +101 -0
  37. package/src/sandbox/channel-a.ts +34 -7
  38. package/src/sandbox/viewer.ts +47 -11
  39. package/dist/chunk-3TP54PPX.js.map +0 -1
@@ -1,5 +1,4 @@
1
1
  import {
2
- AddOrganizationWorkspaceMemberRequest,
3
2
  AcceptOrganizationInvitationRequest,
4
3
  CreateOrganizationRequest,
5
4
  CreateOrganizationResponse,
@@ -8,24 +7,28 @@ import {
8
7
  ListManagedOrganizationMembershipsResponse,
9
8
  ListOrganizationInvitationsPageQuery,
10
9
  ListOrganizationInvitationsPageResponse,
11
- ListOrganizationMembersResponse,
10
+ ListOrganizationAdministrationMembersResponse,
12
11
  OrganizationAdministrationOverview,
13
12
  OrganizationInvitation,
13
+ OrganizationUserSetupDelivery,
14
14
  OrganizationMember,
15
15
  OrganizationPrivateSessionSettings,
16
16
  OrganizationRetentionPolicy,
17
17
  OrganizationSummary,
18
- Permission,
18
+ OrganizationWorkspaceAccess,
19
+ OrganizationWorkspaceAccessMember,
20
+ PutOrganizationWorkspaceMemberRequest,
19
21
  RevokeOrganizationInvitationRequest,
22
+ RetryOrganizationUserSetupDeliveryRequest,
23
+ RevokeOrganizationWorkspaceMemberRequest,
24
+ RevokeOrganizationWorkspaceMemberResponse,
20
25
  UpdateOrganizationMemberRequest,
21
26
  UpdateOrganizationNameRequest,
22
27
  UpdateOrganizationPrivateSessionSettingsRequest,
23
28
  UpdateOrganizationRetentionPolicyRequest,
24
- UpdateWorkspaceMemberRequest,
25
- UpdateWorkspaceRequest,
29
+ UpdateOrganizationWorkspaceRequest,
26
30
  UpdateWorkspaceSettingsRequest,
27
31
  Workspace,
28
- WorkspaceMember,
29
32
  } from "@opengeni/contracts";
30
33
  import {
31
34
  getManagedSession,
@@ -35,33 +38,43 @@ import {
35
38
  import {
36
39
  acceptOrganizationInvitation,
37
40
  bindPendingOrganizationInvitationsForVerifiedEmail,
41
+ claimOrganizationUserSetupDelivery,
38
42
  createManagedOrganization,
39
- createOrganizationSharedWorkspace,
43
+ createOrganizationWorkspace,
40
44
  createOrganizationInvitation,
41
45
  ensureManagedAccessForUserWithOrganizationMemberships,
42
- getManagedUserProfilesByIds,
43
46
  getOrganizationAdministrationOverview,
47
+ getOrganizationInvitationForAdministration,
44
48
  getOrganizationPrivateSessionSettings,
45
49
  getSelfOrganizationInvitation,
46
50
  getOrganizationRetentionPolicy,
47
- listOrganizationMembers,
51
+ listOrganizationAdministrationMembers,
48
52
  listOrganizationInvitations,
49
53
  listSelfOrganizationInvitations,
50
54
  nestedPostgresSqlState,
55
+ prepareOrganizationUserSetupDelivery,
51
56
  revokeOrganizationInvitation,
52
- removeWorkspaceMember,
53
- updateOrganizationSharedWorkspace,
57
+ settleOrganizationUserSetupDelivery,
58
+ putOrganizationWorkspaceMember,
59
+ revokeOrganizationWorkspaceMember,
60
+ updateOrganizationWorkspace,
54
61
  updateOrganizationSharedWorkspaceSettings,
55
62
  updateOrganizationMember,
56
63
  updateOrganizationName,
57
64
  updateOrganizationPrivateSessionSettings,
58
65
  updateOrganizationRetentionPolicy,
59
- upsertOrganizationSharedWorkspaceMember,
60
66
  } from "@opengeni/db";
61
67
  import type { Context, Hono } from "hono";
62
68
  import { HTTPException } from "hono/http-exception";
63
69
  import { z } from "zod";
64
70
 
71
+ import {
72
+ assertOrganizationUserSetupDeliveryConfigured,
73
+ deriveOrganizationUserSetupToken,
74
+ organizationUserSetupPayloadDigest,
75
+ renderOrganizationUserSetupEmail,
76
+ } from "../auth/organization-user-setup";
77
+
65
78
  const OrganizationId = z.string().uuid();
66
79
  const WorkspaceId = z.string().uuid();
67
80
  const MembershipId = z.string().uuid();
@@ -118,19 +131,6 @@ function rethrowMembershipError(error: unknown): never {
118
131
  throw error;
119
132
  }
120
133
 
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
134
  export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDeps): void {
135
135
  app.post("/v1/organizations", async (context) => {
136
136
  const { session, subjectId } = await requireManagedHuman(context, deps);
@@ -159,6 +159,7 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
159
159
  email: session.user.email,
160
160
  name: session.user.name,
161
161
  emailVerified: session.user.emailVerified,
162
+ provisionFallbackOrganization: false,
162
163
  });
163
164
  return context.json(
164
165
  ListManagedOrganizationMembershipsResponse.parse({
@@ -166,6 +167,11 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
166
167
  }),
167
168
  );
168
169
  } catch (error) {
170
+ // Not dead after 0348: a terminal-only membership is now a bounded EMPTY
171
+ // projection rather than a 42501, but the verified-email binder still
172
+ // raises 42501 when the session claims `emailVerified` and the durable
173
+ // `auth_users` row disagrees (an email change clears it), so this stays
174
+ // the correct deny for a stale-verification session.
169
175
  if (nestedPostgresSqlState(error) === "42501") {
170
176
  throw new HTTPException(403, {
171
177
  message: "organization membership is not active",
@@ -256,21 +262,17 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
256
262
  "organization id",
257
263
  );
258
264
  const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
259
- const payload = await parseBody(context, UpdateWorkspaceRequest);
265
+ const payload = await parseBody(context, UpdateOrganizationWorkspaceRequest);
260
266
  try {
261
267
  return context.json(
262
- Workspace.parse(
263
- await updateOrganizationSharedWorkspace(deps.db, {
268
+ OrganizationWorkspaceAccess.parse(
269
+ await updateOrganizationWorkspace(deps.db, {
264
270
  organizationId,
265
271
  workspaceId,
266
272
  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
- : {}),
273
+ name: payload.name.trim(),
274
+ expectedUpdatedAt: payload.expectedUpdatedAt,
275
+ operationId: payload.operationId,
274
276
  }),
275
277
  ),
276
278
  );
@@ -289,15 +291,11 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
289
291
  const payload = await parseBody(context, CreateOrganizationWorkspaceRequest);
290
292
  try {
291
293
  return context.json(
292
- Workspace.parse(
293
- await createOrganizationSharedWorkspace(deps.db, {
294
+ OrganizationWorkspaceAccess.parse(
295
+ await createOrganizationWorkspace(deps.db, {
294
296
  organizationId,
295
297
  actorSubjectId: subjectId,
296
298
  name: payload.name.trim(),
297
- ...(payload.slug !== undefined ? { slug: payload.slug?.trim() || null } : {}),
298
- ...(payload.agentInstructions !== undefined
299
- ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions) }
300
- : {}),
301
299
  operationId: payload.operationId,
302
300
  }),
303
301
  ),
@@ -336,121 +334,69 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
336
334
  },
337
335
  );
338
336
 
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",
337
+ app.put(
338
+ "/v1/organizations/:organizationId/workspaces/:workspaceId/members/:membershipId",
387
339
  async (context) => {
388
- const { subjectId: actorSubjectId } = await requireManagedHuman(context, deps);
340
+ const { subjectId } = await requireManagedHuman(context, deps);
389
341
  const organizationId = parseId(
390
342
  OrganizationId,
391
343
  context.req.param("organizationId"),
392
344
  "organization id",
393
345
  );
394
346
  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);
347
+ const membershipId = parseId(
348
+ MembershipId,
349
+ context.req.param("membershipId"),
350
+ "membership id",
351
+ );
352
+ const payload = await parseBody(context, PutOrganizationWorkspaceMemberRequest);
397
353
  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
354
  return context.json(
414
- WorkspaceMember.parse(
415
- await upsertOrganizationSharedWorkspaceMember(deps.db, {
355
+ OrganizationWorkspaceAccessMember.parse(
356
+ await putOrganizationWorkspaceMember(deps.db, {
416
357
  organizationId,
417
358
  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,
359
+ actorSubjectId: subjectId,
360
+ targetOrganizationMembershipId: membershipId,
361
+ access: payload,
424
362
  }),
425
363
  ),
426
364
  );
427
365
  } catch (error) {
428
- if (error instanceof HTTPException) throw error;
429
366
  rethrowMembershipError(error);
430
367
  }
431
368
  },
432
369
  );
433
370
 
434
- app.delete(
435
- "/v1/organizations/:organizationId/workspaces/:workspaceId/members/:subjectId",
371
+ app.post(
372
+ "/v1/organizations/:organizationId/workspaces/:workspaceId/members/:membershipId/revoke",
436
373
  async (context) => {
437
- const { subjectId: actorSubjectId } = await requireManagedHuman(context, deps);
374
+ const { subjectId } = await requireManagedHuman(context, deps);
438
375
  const organizationId = parseId(
439
376
  OrganizationId,
440
377
  context.req.param("organizationId"),
441
378
  "organization id",
442
379
  );
443
380
  const workspaceId = parseId(WorkspaceId, context.req.param("workspaceId"), "workspace id");
444
- const targetSubjectId = decodeURIComponent(context.req.param("subjectId"));
381
+ const membershipId = parseId(
382
+ MembershipId,
383
+ context.req.param("membershipId"),
384
+ "membership id",
385
+ );
386
+ const payload = await parseBody(context, RevokeOrganizationWorkspaceMemberRequest);
445
387
  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);
388
+ return context.json(
389
+ RevokeOrganizationWorkspaceMemberResponse.parse(
390
+ await revokeOrganizationWorkspaceMember(deps.db, {
391
+ organizationId,
392
+ workspaceId,
393
+ actorSubjectId: subjectId,
394
+ targetOrganizationMembershipId: membershipId,
395
+ expectedUpdatedAt: payload.expectedUpdatedAt,
396
+ operationId: payload.operationId,
397
+ }),
398
+ ),
399
+ );
454
400
  } catch (error) {
455
401
  rethrowMembershipError(error);
456
402
  }
@@ -514,19 +460,42 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
514
460
  "organization id",
515
461
  );
516
462
  const payload = await parseBody(context, CreateOrganizationInvitationRequest);
463
+ // Bearer construction configuration must fail before the invitation
464
+ // commits. Provider outcomes are journaled durably after this boundary.
517
465
  try {
466
+ assertOrganizationUserSetupDeliveryConfigured(deps.settings, deps.managedEmailTransport);
467
+ } catch {
468
+ throw new HTTPException(503, {
469
+ message: "invited-user account setup delivery is not configured on this deployment",
470
+ });
471
+ }
472
+ try {
473
+ const invitation = OrganizationInvitation.parse(
474
+ await createOrganizationInvitation(deps.db, {
475
+ organizationId,
476
+ actorSubjectId: subjectId,
477
+ operationId: payload.operationId,
478
+ targetSubjectId: null,
479
+ targetEmail: payload.email.trim().toLowerCase(),
480
+ ...(payload.name === undefined ? {} : { targetName: payload.name }),
481
+ initialWorkspaceIds: payload.initialWorkspaceIds,
482
+ role: payload.role,
483
+ expiresAt: payload.expiresAt,
484
+ }),
485
+ );
486
+ await deliverOrganizationUserSetup(deps, {
487
+ organizationId,
488
+ actorSubjectId: subjectId,
489
+ invitationId: invitation.id,
490
+ invitationOperationId: payload.operationId,
491
+ operationId: payload.operationId,
492
+ });
518
493
  return context.json(
519
494
  OrganizationInvitation.parse(
520
- await createOrganizationInvitation(deps.db, {
495
+ await getOrganizationInvitationForAdministration(deps.db, {
521
496
  organizationId,
522
497
  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,
498
+ invitationId: invitation.id,
530
499
  }),
531
500
  ),
532
501
  201,
@@ -566,6 +535,45 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
566
535
  }
567
536
  });
568
537
 
538
+ app.post(
539
+ "/v1/organizations/:organizationId/invitations/:invitationId/delivery/retry",
540
+ 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 invitationId = parseId(
548
+ InvitationId,
549
+ context.req.param("invitationId"),
550
+ "invitation id",
551
+ );
552
+ const payload = await parseBody(context, RetryOrganizationUserSetupDeliveryRequest);
553
+ try {
554
+ assertOrganizationUserSetupDeliveryConfigured(deps.settings, deps.managedEmailTransport);
555
+ } catch {
556
+ throw new HTTPException(503, {
557
+ message: "invited-user account setup delivery is not configured on this deployment",
558
+ });
559
+ }
560
+ try {
561
+ return context.json(
562
+ OrganizationUserSetupDelivery.parse(
563
+ await deliverOrganizationUserSetup(deps, {
564
+ organizationId,
565
+ actorSubjectId: subjectId,
566
+ invitationId,
567
+ operationId: payload.operationId,
568
+ }),
569
+ ),
570
+ );
571
+ } catch (error) {
572
+ rethrowMembershipError(error);
573
+ }
574
+ },
575
+ );
576
+
569
577
  app.post("/v1/organization-invitations/:invitationId/accept", async (context) => {
570
578
  const { session, subjectId } = await requireManagedHuman(context, deps);
571
579
  const invitationId = parseId(InvitationId, context.req.param("invitationId"), "invitation id");
@@ -639,26 +647,11 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
639
647
  "organization id",
640
648
  );
641
649
  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
650
  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
- };
651
+ ListOrganizationAdministrationMembersResponse.parse({
652
+ members: await listOrganizationAdministrationMembers(deps.db, {
653
+ organizationId,
654
+ actorSubjectId: subjectId,
662
655
  }),
663
656
  }),
664
657
  );
@@ -740,3 +733,67 @@ export function registerOrganizationMembershipRoutes(app: Hono, deps: ApiRouteDe
740
733
  }
741
734
  });
742
735
  }
736
+
737
+ async function deliverOrganizationUserSetup(
738
+ deps: ApiRouteDeps,
739
+ input: {
740
+ organizationId: string;
741
+ actorSubjectId: string;
742
+ invitationId: string;
743
+ invitationOperationId?: string;
744
+ operationId: string;
745
+ },
746
+ ) {
747
+ const claim = await claimOrganizationUserSetupDelivery(deps.db, input);
748
+ if (!claim.claimed) return claim.delivery;
749
+ const setup = await deriveOrganizationUserSetupToken(deps.settings, {
750
+ invitationId: claim.invitationId,
751
+ deliveryId: claim.delivery.id,
752
+ });
753
+ const message = renderOrganizationUserSetupEmail({
754
+ senderEmail: deps.managedEmailTransport.sender,
755
+ recipientEmail: claim.recipientEmail,
756
+ recipientName: claim.recipientName,
757
+ organizationName: claim.organizationName,
758
+ organizationRole: claim.organizationRole,
759
+ sharedWorkspaceAccess: claim.sharedWorkspaceAccess,
760
+ setupUrl: setup.url,
761
+ });
762
+ await prepareOrganizationUserSetupDelivery(deps.db, {
763
+ organizationId: input.organizationId,
764
+ actorSubjectId: input.actorSubjectId,
765
+ deliveryId: claim.delivery.id,
766
+ attemptId: claim.attemptId,
767
+ claimHolderId: claim.claimHolderId,
768
+ tokenDigest: setup.digest,
769
+ payloadDigest: await organizationUserSetupPayloadDigest({
770
+ ...message,
771
+ providerIdempotencyScope: deps.managedEmailTransport.idempotency.scope,
772
+ }),
773
+ providerIdempotencyScope: deps.managedEmailTransport.idempotency.scope,
774
+ providerIdempotencyRetentionSeconds: deps.managedEmailTransport.idempotency.retentionSeconds,
775
+ });
776
+ let outcome:
777
+ | { status: "sent"; providerMessageId: string | null }
778
+ | { status: "failed" | "outcome_unknown"; errorClass: string };
779
+ try {
780
+ outcome = await deps.managedEmailTransport.send({
781
+ kind: "organization_user_setup",
782
+ ...message,
783
+ idempotencyKey: claim.providerKey,
784
+ });
785
+ } catch {
786
+ outcome = { status: "outcome_unknown", errorClass: "transport_threw" };
787
+ }
788
+ return await settleOrganizationUserSetupDelivery(deps.db, {
789
+ organizationId: input.organizationId,
790
+ actorSubjectId: input.actorSubjectId,
791
+ deliveryId: claim.delivery.id,
792
+ attemptId: claim.attemptId,
793
+ claimHolderId: claim.claimHolderId,
794
+ outcome: outcome.status,
795
+ ...(outcome.status === "sent"
796
+ ? { providerMessageId: outcome.providerMessageId }
797
+ : { errorClass: outcome.errorClass }),
798
+ });
799
+ }