@opengeni/db 1.0.1 → 3.2.0-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 (271) hide show
  1. package/dist/attached-browser-devices.d.ts +7 -0
  2. package/dist/automations.d.ts +154 -0
  3. package/dist/canonical-human-identities.js +3 -3
  4. package/dist/capability-integrations.d.ts +13 -0
  5. package/dist/child-lifecycle-notices.d.ts +68 -0
  6. package/dist/{chunk-4SR4HKKA.js → chunk-2EH4WRHL.js} +19 -11
  7. package/dist/chunk-2EH4WRHL.js.map +1 -0
  8. package/dist/{chunk-CUPM3FGB.js → chunk-2PRS4J5Q.js} +3 -3
  9. package/dist/chunk-6HMM2R5T.js +390 -0
  10. package/dist/chunk-6HMM2R5T.js.map +1 -0
  11. package/dist/{chunk-ZWREYEWA.js → chunk-AZHW7R5V.js} +56 -11
  12. package/dist/chunk-AZHW7R5V.js.map +1 -0
  13. package/dist/{chunk-LTG3S6ZY.js → chunk-E6CLMKEE.js} +2 -2
  14. package/dist/{chunk-6ZYKCRFO.js → chunk-KRLPEMLH.js} +7591 -4417
  15. package/dist/chunk-KRLPEMLH.js.map +1 -0
  16. package/dist/chunk-NAB6FKFN.js +516 -0
  17. package/dist/chunk-NAB6FKFN.js.map +1 -0
  18. package/dist/{chunk-SK7YYHBI.js → chunk-SIBYBA2D.js} +46 -1
  19. package/dist/chunk-SIBYBA2D.js.map +1 -0
  20. package/dist/chunk-UZNXG44K.js +4508 -0
  21. package/dist/chunk-UZNXG44K.js.map +1 -0
  22. package/dist/codemode-operations.d.ts +1 -6
  23. package/dist/company-brain-context-selection-schema.d.ts +705 -0
  24. package/dist/company-brain-context-selection.d.ts +94 -0
  25. package/dist/company-brain-governed-writes.d.ts +24 -0
  26. package/dist/company-brain.d.ts +55 -0
  27. package/dist/company-profile-agent-admin.d.ts +15 -0
  28. package/dist/company-profile-schema.d.ts +619 -2
  29. package/dist/company-profile.d.ts +9 -4
  30. package/dist/connection-authority.d.ts +95 -0
  31. package/dist/connection-scopes.d.ts +8 -0
  32. package/dist/connection-tenancy-backfill.d.ts +85 -0
  33. package/dist/connection-token-resolver.d.ts +30 -1
  34. package/dist/database.d.ts +23 -4
  35. package/dist/editable-artifact-durable-export.js +2 -2
  36. package/dist/editable-artifacts.d.ts +2 -2
  37. package/dist/editable-artifacts.js +3 -3
  38. package/dist/governed-learning-activation-schema.d.ts +984 -0
  39. package/dist/governed-learning-activation.d.ts +98 -0
  40. package/dist/governed-learning-evaluator-schema.d.ts +633 -0
  41. package/dist/governed-learning-evaluator.d.ts +28 -0
  42. package/dist/index.d.ts +1606 -81
  43. package/dist/index.js +21898 -8147
  44. package/dist/index.js.map +1 -1
  45. package/dist/insights.d.ts +18 -0
  46. package/dist/integration-bindings.d.ts +1 -0
  47. package/dist/integration-facets.d.ts +30 -0
  48. package/dist/interaction-placement-loss.d.ts +22 -0
  49. package/dist/managed-human-provisioning.d.ts +25 -0
  50. package/dist/memory-domain.d.ts +3 -3
  51. package/dist/migrate.d.ts +11 -1
  52. package/dist/migrate.js +1 -1
  53. package/dist/new-session-drafts.d.ts +43 -5
  54. package/dist/organization-membership-backfill.d.ts +174 -0
  55. package/dist/organization-membership-lifecycle.d.ts +162 -0
  56. package/dist/pr-review.d.ts +107 -0
  57. package/dist/preference-registry-schema.d.ts +2 -0
  58. package/dist/preference-registry.d.ts +2 -0
  59. package/dist/provision-roles.js +1 -1
  60. package/dist/runtime-posture.d.ts +29 -8
  61. package/dist/schema.d.ts +24914 -13703
  62. package/dist/schema.js +99 -1
  63. package/dist/scoped-knowledge-schema.d.ts +52 -1
  64. package/dist/session-background-commands.d.ts +125 -0
  65. package/dist/session-background-commands.js +34 -0
  66. package/dist/session-background-commands.js.map +1 -0
  67. package/dist/session-control.d.ts +156 -3
  68. package/dist/session-goal-pacing.d.ts +68 -0
  69. package/dist/session-queue-commands.d.ts +43 -4
  70. package/dist/session-realtime-ledger.d.ts +2 -0
  71. package/dist/session-tenancy.d.ts +71 -1
  72. package/dist/session-tenancy.js +25 -3
  73. package/dist/session-tool-call-settlement.d.ts +9 -4
  74. package/dist/slack-routing-personal-workspace.d.ts +23 -0
  75. package/dist/slack-routing.d.ts +268 -0
  76. package/dist/slack-task-policy-schema.d.ts +34 -0
  77. package/dist/slack-task-policy.d.ts +8 -0
  78. package/dist/slack-user-link-access.d.ts +28 -0
  79. package/dist/task-notes-schema.d.ts +594 -0
  80. package/dist/task-notes.d.ts +39 -0
  81. package/dist/turn-initiator.d.ts +2 -0
  82. package/dist/user-resource-authority.d.ts +109 -0
  83. package/dist/video-generation.js +3 -3
  84. package/dist/workspace-authority.d.ts +81 -0
  85. package/dist/workspace-instruction-policies-schema.d.ts +279 -0
  86. package/dist/workspace-instruction-policies.d.ts +62 -0
  87. package/dist/workspace-learning-policy.d.ts +1 -0
  88. package/dist/workspace-membership-permissions.d.ts +2 -0
  89. package/drizzle/0240_enrollment_connection_authority.sql +39 -0
  90. package/drizzle/0241_atomic_personal_resource_delegation.sql +1000 -0
  91. package/drizzle/0241_enrollment_agent_runtime.sql +70 -0
  92. package/drizzle/0244_slack_app_home_refresh_queue.sql +120 -0
  93. package/drizzle/0245_model_context_contribution_facts.sql +88 -0
  94. package/drizzle/0246_integration_personal_instance_authority.sql +70 -0
  95. package/drizzle/0247_terraform_stacks_provenance_repair.sql +428 -0
  96. package/drizzle/0248_terraform_stacks_component_resolution_fence.sql +83 -0
  97. package/drizzle/0249_personal_resource_delegation_authority_correction.sql +662 -0
  98. package/drizzle/0250_direct_retained_process_owner_liveness.sql +199 -0
  99. package/drizzle/0251_connected_machine_operation_policy.sql +45 -0
  100. package/drizzle/0252_scheduled_personal_resource_delegation.sql +1867 -0
  101. package/drizzle/0253_common_user_resource_authority_lifecycle.sql +1555 -0
  102. package/drizzle/0254_scoped_variable_set_authority.sql +1255 -0
  103. package/drizzle/0255_company_brain_governed_write_proposals.sql +524 -0
  104. package/drizzle/0256_connection_authority_delegation.sql +703 -0
  105. package/drizzle/0257_goal_revision_decisions_and_root_constraints.sql +436 -0
  106. package/drizzle/0258_three_scope_document_knowledge_authority.sql +1273 -0
  107. package/drizzle/0259_company_brain_context_selection_receipts.sql +869 -0
  108. package/drizzle/0260_task_note_knowledge_promotion.sql +820 -0
  109. package/drizzle/0261_preference_knowledge_proposal_actor_binding.sql +471 -0
  110. package/drizzle/0262_scoped_connected_machines_and_rigs.sql +1700 -0
  111. package/drizzle/0263_organization_membership_lifecycle.sql +3284 -0
  112. package/drizzle/0264_connection_authority_runtime_activation.sql +1084 -0
  113. package/drizzle/0266_company_brain_context_receipt_inspection.sql +156 -0
  114. package/drizzle/0268_governed_learning_decision_receipts.sql +825 -0
  115. package/drizzle/0269_governed_learning_activation_controller.sql +1874 -0
  116. package/drizzle/0270_governed_learning_history_inspection.sql +170 -0
  117. package/drizzle/0271_company_brain_retrieval_only_default.sql +524 -0
  118. package/drizzle/0272_human_confirmed_learning_activation.sql +591 -0
  119. package/drizzle/0273_scheduled_variable_set_materialization.sql +268 -0
  120. package/drizzle/0274_human_confirmed_knowledge_review.sql +310 -0
  121. package/drizzle/0275_scheduled_connection_authority.sql +5527 -0
  122. package/drizzle/0276_onboarding_proposal_initiating_human_guc.sql +198 -0
  123. package/drizzle/0277_workspace_writer_authority_attribution.sql +393 -0
  124. package/drizzle/0278_workspace_membership_removal_fencing.sql +1174 -0
  125. package/drizzle/0279_workspace_connection_use_lane.sql +343 -0
  126. package/drizzle/0280_connection_and_variable_set_audit_attribution.sql +773 -0
  127. package/drizzle/0281_viewer_holder_authority_claims.sql +52 -0
  128. package/drizzle/0282_variable_set_session_attach_attribution.sql +178 -0
  129. package/drizzle/0283_editable_spreadsheet_authored_state.sql +82 -0
  130. package/drizzle/0284_truthful_human_confirmed_review_reason.sql +930 -0
  131. package/drizzle/0285_organization_tenancy_inventory.sql +329 -0
  132. package/drizzle/0286_widen_task_note_expiry_ceiling.sql +391 -0
  133. package/drizzle/0287_open_suffix_pending_tool_calls.sql +18 -0
  134. package/drizzle/0288_attached_browser_reenrollment.sql +64 -0
  135. package/drizzle/0289_session_composer_policy_authority.sql +597 -0
  136. package/drizzle/0290_organization_membership_backfill.sql +268 -0
  137. package/drizzle/0291_resource_authority_classification_assertion.sql +371 -0
  138. package/drizzle/0292_truthful_tenancy_inventory_counters.sql +291 -0
  139. package/drizzle/0293_confirm_time_rule_rebaseline.sql +615 -0
  140. package/drizzle/0294_preference_activation_authority.sql +237 -0
  141. package/drizzle/0295_retire_legacy_standing_memory_mode.sql +476 -0
  142. package/drizzle/0296_force_rls_backfill_noop_repair.sql +215 -0
  143. package/drizzle/0297_session_ownership_classification_and_backfill.sql +752 -0
  144. package/drizzle/0298_organization_tenancy_parity.sql +609 -0
  145. package/drizzle/0299_organization_membership_lock_order.sql +291 -0
  146. package/drizzle/0300_tenancy_backfill_ledger.sql +459 -0
  147. package/drizzle/0301_session_snapshot_and_pin_visibility.sql +181 -0
  148. package/drizzle/0302_personal_workspace_session_ownership.sql +303 -0
  149. package/drizzle/0303_session_tenancy_product_activation.sql +919 -0
  150. package/drizzle/0304_personal_workspace_private_session_reads.sql +97 -0
  151. package/drizzle/0305_personal_resource_grant_management.sql +584 -0
  152. package/drizzle/0306_atomic_personal_resource_attachments.sql +1374 -0
  153. package/drizzle/0307_session_attention_state.sql +51 -0
  154. package/drizzle/0308_session_archives.sql +35 -0
  155. package/drizzle/0309_channel_project_pins.sql +6 -0
  156. package/drizzle/0310_channel_project_order.sql +27 -0
  157. package/drizzle/0311_company_scope_and_private_session_create.sql +656 -0
  158. package/drizzle/0312_quiescent_session_tree_deletion.sql +39 -0
  159. package/drizzle/0313_private_child_session_authority.sql +365 -0
  160. package/drizzle/0314_unregistered_organization_invitations.sql +606 -0
  161. package/drizzle/0315_personal_github_repository_selection.sql +624 -0
  162. package/drizzle/0316_human_confirmed_activation_resumed_generation.sql +869 -0
  163. package/drizzle/0317_session_goal_continuation_hold.sql +64 -0
  164. package/drizzle/0318_session_turn_startup_milestones.sql +115 -0
  165. package/drizzle/0319_event_triggered_automations.sql +254 -0
  166. package/drizzle/0320_pr_review_pack.sql +113 -0
  167. package/drizzle/0321_slack_bot_environment_display_name.sql +15 -0
  168. package/drizzle/0322_session_turns_unclaimed_prompt_trigger_index.sql +5 -0
  169. package/drizzle/0323_organization_private_session_enablement.sql +489 -0
  170. package/drizzle/0324_human_confirmed_company_profile_agent_admin.sql +856 -0
  171. package/drizzle/0325_child_lifecycle_notices.sql +44 -0
  172. package/drizzle/0326_interaction_operation_error_codes.sql +62 -0
  173. package/drizzle/0327_slack_first_task_hint.sql +29 -0
  174. package/drizzle/0328_session_background_commands.sql +590 -0
  175. package/drizzle/0329_slack_orchestration_delivery_events.sql +91 -0
  176. package/drizzle/0330_api_key_descriptions.sql +20 -0
  177. package/drizzle/0331_managed_organization_creation.sql +163 -0
  178. package/drizzle/0332_organization_shared_workspace_control_plane.sql +539 -0
  179. package/drizzle/0333_session_turn_prompt_routing.sql +45 -0
  180. package/drizzle/0334_connected_machine_workspace_root.sql +99 -0
  181. package/drizzle/0335_slack_workspace_routing.sql +460 -0
  182. package/drizzle/0336_atomic_session_fork_visibility.sql +667 -0
  183. package/drizzle/0337_slack_routed_action_handles.sql +189 -0
  184. package/drizzle/0338_atomic_connected_machine_attachments.sql +410 -0
  185. package/drizzle/0339_document_authority_reclassification.sql +1153 -0
  186. package/drizzle/0340_tenancy_backfill_activation_evidence.sql +1347 -0
  187. package/drizzle/0341_slack_routing_probe_organization_fence.sql +109 -0
  188. package/drizzle/0342_slack_route_prompt_single_pending.sql +32 -0
  189. package/drizzle/0343_personal_document_force_rls_lock_repair.sql +146 -0
  190. package/drizzle/0344_private_session_visibility_transition_gate.sql +85 -0
  191. package/drizzle/0345_tenant_scoped_session_tenancy_fence.sql +2839 -0
  192. package/drizzle/0346_document_migration_audit_surface.sql +454 -0
  193. package/drizzle/0347_connection_authority_convergence_evidence.sql +396 -0
  194. package/package.json +10 -6
  195. package/src/attached-browser-devices.ts +233 -5
  196. package/src/automations.ts +911 -0
  197. package/src/browser-sessions.ts +10 -0
  198. package/src/capability-integrations.ts +175 -1
  199. package/src/child-lifecycle-notices.ts +241 -0
  200. package/src/codemode-operations.ts +1 -35
  201. package/src/company-brain-context-selection-schema.ts +100 -0
  202. package/src/company-brain-context-selection.ts +359 -0
  203. package/src/company-brain-governed-writes.ts +1170 -0
  204. package/src/company-brain.ts +309 -0
  205. package/src/company-profile-agent-admin.ts +225 -0
  206. package/src/company-profile-schema.ts +121 -1
  207. package/src/company-profile.ts +21 -0
  208. package/src/computer-sessions.ts +10 -1
  209. package/src/connection-authority.ts +290 -0
  210. package/src/connection-scopes.ts +20 -0
  211. package/src/connection-tenancy-backfill.ts +167 -0
  212. package/src/connection-token-resolver.ts +263 -31
  213. package/src/database.ts +87 -2
  214. package/src/editable-artifacts-schema.ts +10 -5
  215. package/src/editable-artifacts.ts +21 -11
  216. package/src/governed-learning-activation-schema.ts +121 -0
  217. package/src/governed-learning-activation.ts +588 -0
  218. package/src/governed-learning-evaluator-schema.ts +72 -0
  219. package/src/governed-learning-evaluator.ts +217 -0
  220. package/src/index.ts +11676 -1550
  221. package/src/insights.ts +107 -0
  222. package/src/integration-bindings.ts +88 -6
  223. package/src/integration-facets.ts +287 -84
  224. package/src/interaction-placement-loss.ts +234 -0
  225. package/src/managed-human-provisioning.ts +116 -0
  226. package/src/memory-domain.ts +27 -32
  227. package/src/migrate.ts +77 -1
  228. package/src/new-session-drafts.ts +229 -35
  229. package/src/organization-membership-backfill.ts +735 -0
  230. package/src/organization-membership-lifecycle.ts +995 -0
  231. package/src/pr-review.ts +890 -0
  232. package/src/preference-registry.ts +31 -1
  233. package/src/provision-roles.ts +905 -6
  234. package/src/runtime-posture-cli.ts +2 -0
  235. package/src/runtime-posture.ts +1317 -72
  236. package/src/schema.ts +3316 -428
  237. package/src/scoped-knowledge-schema.ts +16 -8
  238. package/src/scoped-knowledge.ts +1 -1
  239. package/src/session-background-commands.ts +759 -0
  240. package/src/session-control.ts +640 -74
  241. package/src/session-goal-pacing.ts +126 -0
  242. package/src/session-queue-commands.ts +441 -85
  243. package/src/session-realtime-context.ts +4 -6
  244. package/src/session-realtime-ledger.ts +28 -14
  245. package/src/session-realtime-mirror.ts +1 -1
  246. package/src/session-tenancy.ts +410 -49
  247. package/src/session-tool-call-settlement.ts +38 -29
  248. package/src/slack-routing-personal-workspace.ts +67 -0
  249. package/src/slack-routing.ts +867 -0
  250. package/src/slack-task-policy-schema.ts +5 -0
  251. package/src/slack-task-policy.ts +18 -0
  252. package/src/slack-user-link-access.ts +138 -11
  253. package/src/task-notes-schema.ts +74 -0
  254. package/src/task-notes.ts +124 -2
  255. package/src/turn-initiator.ts +8 -0
  256. package/src/user-resource-authority.ts +377 -0
  257. package/src/workspace-authority.ts +160 -0
  258. package/src/workspace-instruction-policies-schema.ts +53 -1
  259. package/src/workspace-instruction-policies.ts +528 -77
  260. package/src/workspace-learning-policy.ts +21 -5
  261. package/src/workspace-membership-permissions.ts +9 -0
  262. package/dist/chunk-4SR4HKKA.js.map +0 -1
  263. package/dist/chunk-6ZYKCRFO.js.map +0 -1
  264. package/dist/chunk-NXAFLCQX.js +0 -2468
  265. package/dist/chunk-NXAFLCQX.js.map +0 -1
  266. package/dist/chunk-SK7YYHBI.js.map +0 -1
  267. package/dist/chunk-WD5ROSLL.js +0 -117
  268. package/dist/chunk-WD5ROSLL.js.map +0 -1
  269. package/dist/chunk-ZWREYEWA.js.map +0 -1
  270. /package/dist/{chunk-CUPM3FGB.js.map → chunk-2PRS4J5Q.js.map} +0 -0
  271. /package/dist/{chunk-LTG3S6ZY.js.map → chunk-E6CLMKEE.js.map} +0 -0
@@ -0,0 +1,995 @@
1
+ import {
2
+ CreateOrganizationResponse,
3
+ ListOrganizationInvitationsResponse,
4
+ ListOrganizationInvitationsPageResponse,
5
+ ListOrganizationMembersResponse,
6
+ ListSelfOrganizationMembershipsResponse,
7
+ OrganizationInvitation,
8
+ OrganizationAdministrationOverview,
9
+ OrganizationPrivateSessionSettings,
10
+ OrganizationMember,
11
+ OrganizationRetentionDeletionClaim,
12
+ OrganizationRetentionDatabaseFinalization,
13
+ OrganizationRetentionDeletionObject,
14
+ OrganizationRetentionDeletionPreview,
15
+ OrganizationRetentionDeletionResult,
16
+ OrganizationRetentionPolicy,
17
+ OrganizationSummary,
18
+ type CreateOrganizationResponse as CreateOrganizationResponseType,
19
+ type OrganizationAdministrationOverview as OrganizationAdministrationOverviewType,
20
+ type OrganizationInvitation as OrganizationInvitationType,
21
+ type OrganizationMember as OrganizationMemberType,
22
+ type OrganizationPrivateSessionSettings as OrganizationPrivateSessionSettingsType,
23
+ type OrganizationMembershipRole,
24
+ type OrganizationRetentionDeletionClaim as OrganizationRetentionDeletionClaimType,
25
+ type OrganizationRetentionDatabaseFinalization as OrganizationRetentionDatabaseFinalizationType,
26
+ type OrganizationRetentionDeletionObject as OrganizationRetentionDeletionObjectType,
27
+ type OrganizationRetentionDeletionPreview as OrganizationRetentionDeletionPreviewType,
28
+ type OrganizationRetentionDeletionResult as OrganizationRetentionDeletionResultType,
29
+ type OrganizationRetentionPolicy as OrganizationRetentionPolicyType,
30
+ type OrganizationSummary as OrganizationSummaryType,
31
+ type UpdateOrganizationMemberRequest,
32
+ } from "@opengeni/contracts";
33
+ import { and, eq, sql } from "drizzle-orm";
34
+ import type { Database } from "./database";
35
+ import {
36
+ rawRows,
37
+ setSubjectRlsContext,
38
+ withRestoredSessionActivityRlsContext,
39
+ withRlsContext,
40
+ } from "./database";
41
+ import { nestedPostgresSqlState } from "./persistence-errors";
42
+ import { lockSessionEventWriteRows } from "./session-control";
43
+ import { closePendingSessionToolCallsInTransaction } from "./session-tool-call-settlement";
44
+ import * as schema from "./schema";
45
+
46
+ export async function createManagedOrganization(
47
+ db: Database,
48
+ input: {
49
+ subjectId: string;
50
+ subjectLabel: string;
51
+ name: string;
52
+ operationId: string;
53
+ },
54
+ ): Promise<CreateOrganizationResponseType> {
55
+ return await db.transaction(async (tx) => {
56
+ await setSubjectRlsContext(tx as unknown as Database, input.subjectId);
57
+ const [row] = await rawRows<{ result: unknown }>(
58
+ tx,
59
+ sql`select create_managed_organization(
60
+ ${input.subjectId},
61
+ ${input.subjectLabel},
62
+ ${input.name},
63
+ ${input.operationId}::uuid
64
+ ) as result`,
65
+ );
66
+ return CreateOrganizationResponse.parse(row?.result);
67
+ });
68
+ }
69
+
70
+ /**
71
+ * Bounded replay of one exact organization-lifecycle transaction after a
72
+ * PostgreSQL deadlock abort.
73
+ *
74
+ * A lifecycle command spans an entire organization and therefore touches many
75
+ * rows that ordinary workspace writers touch too, so it can land in a lock
76
+ * cycle with one. PostgreSQL resolves a cycle by aborting one of the two
77
+ * transactions with `40P01`.
78
+ *
79
+ * Replay is exact rather than approximate: the whole lifecycle command runs in
80
+ * one transaction keyed by its caller-supplied operation id
81
+ * (`organization_membership_operation_receipts`) plus its CAS revisions, and a
82
+ * deadlock abort rolls back every durable effect, so re-running the identical
83
+ * command either applies it once or observes the newer authoritative state.
84
+ *
85
+ * Only `40P01` is replayed. `40001` is the lifecycle's own authoritative
86
+ * stale-revision / stale-epoch conflict and must reach the caller unchanged,
87
+ * and the original failure is rethrown untouched once the budget is spent.
88
+ *
89
+ * SCOPE - read this before assuming a deadlock cannot escape. PostgreSQL picks
90
+ * ONE of the two transactions in the cycle as the victim, and it may pick the
91
+ * ordinary workspace writer instead of the lifecycle command. That writer is a
92
+ * plain caller of an unrelated module (`transitionSessionVisibility`, a session
93
+ * event/goal/system-update insert, ...); nothing here can replay it. So this
94
+ * covers exactly the lifecycle side of a cycle: it is a caller-side safety net,
95
+ * NOT a lock-order fix. The known organization/workspace lock-order inversion
96
+ * was removed in SQL by migration
97
+ * `0299_organization_membership_lock_order.sql` (advisory-lock mutual exclusion
98
+ * plus a downgraded `managed_accounts FOR KEY SHARE` row lock), and its
99
+ * parallel-load probe reads `pg_stat_database.deadlocks` directly so this
100
+ * replay cannot mask a regression. Do not treat this wrapper as licence to
101
+ * reintroduce a conflicting organization row lock.
102
+ *
103
+ * Every lifecycle entry point whose command acquires workspace rows - and so
104
+ * can be inside the cycle at all - is wrapped: `accept` (it inserts the
105
+ * personal workspace) and `suspend`/`offboard` (they take the account's
106
+ * `workspaces` rows `FOR KEY SHARE`). The remaining actions never reach a
107
+ * workspace row, so they can block an ordinary writer but cannot close a cycle
108
+ * with one.
109
+ *
110
+ * The wrapper must stay OUTSIDE any transaction it replays: retrying inside an
111
+ * already-aborted transaction is not a retry.
112
+ */
113
+ async function withOrganizationLifecycleDeadlockReplay<T>(operation: () => Promise<T>): Promise<T> {
114
+ const maxAttempts = 3;
115
+ for (let attempt = 1; ; attempt += 1) {
116
+ try {
117
+ return await operation();
118
+ } catch (error) {
119
+ if (attempt >= maxAttempts || nestedPostgresSqlState(error) !== "40P01") throw error;
120
+ await new Promise((resolve) => setTimeout(resolve, 5 + Math.random() * 20));
121
+ }
122
+ }
123
+ }
124
+
125
+ type CommandBase = {
126
+ organizationId: string;
127
+ actorSubjectId: string;
128
+ operationId: string;
129
+ };
130
+
131
+ type OrganizationMembershipProtocolSettlement = {
132
+ accountId: string;
133
+ workspaceId: string;
134
+ sessionId: string;
135
+ turnId: string;
136
+ executionGeneration: number;
137
+ turnAssociation: "current" | null;
138
+ };
139
+
140
+ const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
141
+
142
+ function parseOrganizationMembershipProtocolSettlements(
143
+ value: unknown,
144
+ ): OrganizationMembershipProtocolSettlement[] {
145
+ if (!Array.isArray(value)) {
146
+ throw new Error("Organization membership protocol preparation returned a non-array result");
147
+ }
148
+ return value.map((candidate) => {
149
+ if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) {
150
+ throw new Error("Organization membership protocol preparation returned an invalid item");
151
+ }
152
+ const item = candidate as Record<string, unknown>;
153
+ if (
154
+ typeof item.accountId !== "string" ||
155
+ !uuidPattern.test(item.accountId) ||
156
+ typeof item.workspaceId !== "string" ||
157
+ !uuidPattern.test(item.workspaceId) ||
158
+ typeof item.sessionId !== "string" ||
159
+ !uuidPattern.test(item.sessionId) ||
160
+ typeof item.turnId !== "string" ||
161
+ !uuidPattern.test(item.turnId) ||
162
+ typeof item.executionGeneration !== "number" ||
163
+ !Number.isSafeInteger(item.executionGeneration) ||
164
+ item.executionGeneration <= 0 ||
165
+ (item.turnAssociation !== "current" && item.turnAssociation !== null)
166
+ ) {
167
+ throw new Error("Organization membership protocol preparation returned an invalid item");
168
+ }
169
+ return {
170
+ accountId: item.accountId,
171
+ workspaceId: item.workspaceId,
172
+ sessionId: item.sessionId,
173
+ turnId: item.turnId,
174
+ executionGeneration: item.executionGeneration,
175
+ turnAssociation: item.turnAssociation,
176
+ };
177
+ });
178
+ }
179
+
180
+ async function prepareOrganizationMembershipProtocolSettlements(
181
+ db: Database,
182
+ command: CommandBase & Record<string, unknown>,
183
+ ): Promise<OrganizationMembershipProtocolSettlement[]> {
184
+ return await withRlsContext(
185
+ db,
186
+ { accountId: command.organizationId, workspaceId: null },
187
+ async (scopedDb) => {
188
+ await setSubjectRlsContext(scopedDb, command.actorSubjectId);
189
+ const [row] = await rawRows<{ result: unknown }>(
190
+ scopedDb,
191
+ sql`select prepare_organization_membership_protocol_settlements(
192
+ ${JSON.stringify(command)}::jsonb
193
+ ) as result`,
194
+ );
195
+ return parseOrganizationMembershipProtocolSettlements(row?.result ?? []);
196
+ },
197
+ );
198
+ }
199
+
200
+ async function settleOrganizationMembershipProtocols(
201
+ db: Database,
202
+ settlements: OrganizationMembershipProtocolSettlement[],
203
+ ): Promise<void> {
204
+ const [prior] = await rawRows<{ subject_id: string; initiating_human_subject_id: string }>(
205
+ db,
206
+ sql`select
207
+ coalesce(current_setting('opengeni.subject_id', true), '') as subject_id,
208
+ coalesce(
209
+ current_setting('opengeni.initiating_human_subject_id', true), ''
210
+ ) as initiating_human_subject_id`,
211
+ );
212
+ try {
213
+ // The SECURITY DEFINER preparation has already authorized and locked each
214
+ // exact target turn. Canonical settlement needs the established internal
215
+ // service RLS view because an administrator cannot otherwise see another
216
+ // member's user-private session rows.
217
+ await db.execute(sql`select
218
+ set_config('opengeni.subject_id', '', true),
219
+ set_config('opengeni.initiating_human_subject_id', '', true)`);
220
+ const workspaceIds = [
221
+ ...new Set(settlements.map((settlement) => settlement.workspaceId)),
222
+ ].sort();
223
+ for (const workspaceId of workspaceIds) {
224
+ const workspaceSettlements = settlements
225
+ .filter((settlement) => settlement.workspaceId === workspaceId)
226
+ .sort(
227
+ (left, right) =>
228
+ left.sessionId.localeCompare(right.sessionId) ||
229
+ left.turnId.localeCompare(right.turnId),
230
+ );
231
+ const accountId = workspaceSettlements[0]?.accountId;
232
+ if (!accountId) continue;
233
+ await withRestoredSessionActivityRlsContext(
234
+ db,
235
+ { accountId, workspaceId },
236
+ async (scopedDb) => {
237
+ for (const settlement of workspaceSettlements) {
238
+ const locks = await lockSessionEventWriteRows(scopedDb, {
239
+ workspaceId,
240
+ controlLock: "already_locked",
241
+ workspaceLock: "already_locked",
242
+ sessionIds: [settlement.sessionId],
243
+ turnIds: [settlement.turnId],
244
+ });
245
+ const session = locks.sessions[0];
246
+ const turn = locks.turns[0];
247
+ if (
248
+ !session ||
249
+ !turn ||
250
+ session.accountId !== settlement.accountId ||
251
+ turn.accountId !== settlement.accountId ||
252
+ turn.sessionId !== settlement.sessionId ||
253
+ turn.executionGeneration !== settlement.executionGeneration ||
254
+ !["queued", "running", "requires_action", "recovering", "waiting_capacity"].includes(
255
+ turn.status,
256
+ )
257
+ ) {
258
+ throw new Error("Organization membership protocol settlement lost its locked turn");
259
+ }
260
+ const now = new Date();
261
+ const closedTools = await closePendingSessionToolCallsInTransaction(scopedDb, {
262
+ accountId: settlement.accountId,
263
+ workspaceId,
264
+ sessionId: settlement.sessionId,
265
+ turnId: settlement.turnId,
266
+ reason: "authority_changed",
267
+ sequence: session.lastSequence,
268
+ now,
269
+ turnAssociation: settlement.turnAssociation,
270
+ });
271
+ if (closedTools.sequence !== session.lastSequence) {
272
+ const [updated] = await scopedDb
273
+ .update(schema.sessions)
274
+ .set({ lastSequence: closedTools.sequence, updatedAt: now })
275
+ .where(
276
+ and(
277
+ eq(schema.sessions.accountId, settlement.accountId),
278
+ eq(schema.sessions.workspaceId, workspaceId),
279
+ eq(schema.sessions.id, settlement.sessionId),
280
+ eq(schema.sessions.lastSequence, session.lastSequence),
281
+ ),
282
+ )
283
+ .returning({ id: schema.sessions.id });
284
+ if (!updated) {
285
+ throw new Error("Organization membership protocol sequence changed under its lock");
286
+ }
287
+ }
288
+ }
289
+ },
290
+ );
291
+ }
292
+ } finally {
293
+ await db.execute(sql`select
294
+ set_config('opengeni.subject_id', ${prior?.subject_id ?? ""}, true),
295
+ set_config(
296
+ 'opengeni.initiating_human_subject_id',
297
+ ${prior?.initiating_human_subject_id ?? ""}, true
298
+ )`);
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Remove one workspace membership through the fenced SECURITY DEFINER
304
+ * teardown (migration 0278): the same prepare/settle/command protocol as
305
+ * organization offboarding, scoped to exactly one workspace and one subject.
306
+ * Cancels the removed member's queued/live turns in the workspace, interrupts
307
+ * live attempts, ends their realtime modes, advances their private sessions'
308
+ * authority epochs, registers workflow wakes, deletes their per-workspace
309
+ * personal rows, and deletes the membership - in one transaction. Returns
310
+ * false when no membership row exists (idempotent retry).
311
+ */
312
+ export async function removeWorkspaceMember(
313
+ db: Database,
314
+ input: {
315
+ accountId: string;
316
+ workspaceId: string;
317
+ actorSubjectId: string;
318
+ targetSubjectId: string;
319
+ requireOrganizationSharedWorkspaceAdministration?: boolean;
320
+ },
321
+ ): Promise<boolean> {
322
+ const command = {
323
+ action: "remove",
324
+ organizationId: input.accountId,
325
+ workspaceId: input.workspaceId,
326
+ actorSubjectId: input.actorSubjectId,
327
+ targetSubjectId: input.targetSubjectId,
328
+ operationId: crypto.randomUUID(),
329
+ };
330
+ return await db.transaction(async (tx) => {
331
+ const txDb = tx as unknown as Database;
332
+ let organizationAdministrationCapabilityId: string | null = null;
333
+ if (input.requireOrganizationSharedWorkspaceAdministration) {
334
+ organizationAdministrationCapabilityId = await withRlsContext(
335
+ txDb,
336
+ { accountId: input.accountId, workspaceId: null },
337
+ async (scopedDb) => {
338
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
339
+ const [row] = await rawRows<{ capabilityId: string }>(
340
+ scopedDb,
341
+ sql`select open_organization_shared_workspace_administration_capability(
342
+ ${input.accountId}::uuid,
343
+ ${input.workspaceId}::uuid,
344
+ ${input.actorSubjectId}
345
+ ) as "capabilityId"`,
346
+ );
347
+ if (!row) throw new Error("Organization administration capability was not opened");
348
+ return row.capabilityId;
349
+ },
350
+ );
351
+ }
352
+ // Enter the canonical organization -> session-tenancy prefix before the
353
+ // SECURITY-DEFINER command can lock or mutate session rows. Both locks are
354
+ // reentrant with the command's narrower lifecycle work. The shared tenancy
355
+ // fence lets ordinary writers continue while excluding a concurrent
356
+ // visibility transition/fork for this exact workspace.
357
+ await txDb.execute(
358
+ sql`select pg_advisory_xact_lock(hashtextextended(
359
+ ${`organization-membership:${input.accountId}`}, 0))`,
360
+ );
361
+ await txDb.execute(
362
+ sql`select pg_advisory_xact_lock_shared(hashtextextended(
363
+ ${`session-tenancy:${input.workspaceId}`}, 0))`,
364
+ );
365
+ // The personal-state fence follows that common prefix. Session listing
366
+ // takes its shared counterpart and pin mutation the same exclusive one.
367
+ await txDb.execute(
368
+ sql`select pg_advisory_xact_lock(hashtextextended(
369
+ ${`session-personal-state:${input.workspaceId}:${input.targetSubjectId}`}, 0))`,
370
+ );
371
+ const settlements = await withRlsContext(
372
+ txDb,
373
+ { accountId: input.accountId, workspaceId: null },
374
+ async (scopedDb) => {
375
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
376
+ const [row] = await rawRows<{ result: unknown }>(
377
+ scopedDb,
378
+ sql`select prepare_workspace_membership_removal_settlements(
379
+ ${JSON.stringify(command)}::jsonb
380
+ ) as result`,
381
+ );
382
+ return parseOrganizationMembershipProtocolSettlements(row?.result ?? []);
383
+ },
384
+ );
385
+ await settleOrganizationMembershipProtocols(txDb, settlements);
386
+ const removed = await withRlsContext(
387
+ txDb,
388
+ { accountId: input.accountId, workspaceId: null },
389
+ async (scopedDb) => {
390
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
391
+ const [row] = await rawRows<{ result: unknown }>(
392
+ scopedDb,
393
+ sql`select workspace_membership_removal_command(
394
+ ${JSON.stringify(command)}::jsonb
395
+ ) as result`,
396
+ );
397
+ if (!row) throw new Error("Workspace membership removal returned no result");
398
+ return (row.result as { removed?: unknown } | null)?.removed === true;
399
+ },
400
+ );
401
+ if (organizationAdministrationCapabilityId) {
402
+ await txDb.execute(
403
+ sql`select close_organization_shared_workspace_administration_capability(
404
+ ${organizationAdministrationCapabilityId}::uuid
405
+ )`,
406
+ );
407
+ }
408
+ return removed;
409
+ });
410
+ }
411
+
412
+ export async function assertActiveManagedHumanOrganizationMembership(
413
+ db: Database,
414
+ input: { accountId: string; subjectId: string },
415
+ ): Promise<number | null> {
416
+ const [row] = await rawRows<{ authorization_revision: number | string | null }>(
417
+ db,
418
+ sql`select assert_active_managed_human_organization_membership(
419
+ ${input.accountId}::uuid,
420
+ ${input.subjectId}
421
+ ) as authorization_revision`,
422
+ );
423
+ return row?.authorization_revision === null || row?.authorization_revision === undefined
424
+ ? null
425
+ : Number(row.authorization_revision);
426
+ }
427
+
428
+ async function runCommand(
429
+ db: Database,
430
+ command: CommandBase & Record<string, unknown>,
431
+ ): Promise<unknown> {
432
+ return await withRlsContext(
433
+ db,
434
+ { accountId: command.organizationId, workspaceId: null },
435
+ async (scopedDb) => {
436
+ await setSubjectRlsContext(scopedDb, command.actorSubjectId);
437
+ const [row] = await rawRows<{ result: unknown }>(
438
+ scopedDb,
439
+ sql`select organization_membership_command(${JSON.stringify(command)}::jsonb) as result`,
440
+ );
441
+ if (!row) throw new Error("Organization membership command returned no result");
442
+ return row.result;
443
+ },
444
+ );
445
+ }
446
+
447
+ export async function listSelfOrganizationMemberships(
448
+ db: Database,
449
+ subjectId: string,
450
+ ): Promise<OrganizationMemberType[]> {
451
+ return await db.transaction(async (tx) => {
452
+ await setSubjectRlsContext(tx as unknown as Database, subjectId);
453
+ const [row] = await rawRows<{ result: unknown }>(
454
+ tx,
455
+ sql`select list_self_organization_memberships(${subjectId}) as result`,
456
+ );
457
+ return ListSelfOrganizationMembershipsResponse.parse({
458
+ memberships: row?.result ?? [],
459
+ }).memberships;
460
+ });
461
+ }
462
+
463
+ export async function listSelfOrganizationInvitations(
464
+ db: Database,
465
+ input: { subjectId: string; cursor?: string; limit: number },
466
+ ): Promise<{
467
+ invitations: OrganizationInvitationType[];
468
+ nextCursor: string | null;
469
+ }> {
470
+ return await db.transaction(async (tx) => {
471
+ await setSubjectRlsContext(tx as unknown as Database, input.subjectId);
472
+ const [row] = await rawRows<{ result: unknown }>(
473
+ tx,
474
+ sql`select list_self_organization_invitations(
475
+ ${input.subjectId},
476
+ ${input.cursor ?? null}::uuid,
477
+ ${input.limit}
478
+ ) as result`,
479
+ );
480
+ const candidates = ListOrganizationInvitationsResponse.parse({
481
+ invitations: row?.result ?? [],
482
+ }).invitations;
483
+ const hasMore = candidates.length > input.limit;
484
+ const invitations = candidates.slice(0, input.limit);
485
+ return ListOrganizationInvitationsPageResponse.parse({
486
+ invitations,
487
+ nextCursor: hasMore ? (invitations.at(-1)?.id ?? null) : null,
488
+ });
489
+ });
490
+ }
491
+
492
+ export async function getSelfOrganizationInvitation(
493
+ db: Database,
494
+ input: { subjectId: string; invitationId: string },
495
+ ): Promise<OrganizationInvitationType> {
496
+ return await db.transaction(async (tx) => {
497
+ await setSubjectRlsContext(tx as unknown as Database, input.subjectId);
498
+ const [row] = await rawRows<{ result: unknown }>(
499
+ tx,
500
+ sql`select get_self_organization_invitation(
501
+ ${input.subjectId},
502
+ ${input.invitationId}::uuid
503
+ ) as result`,
504
+ );
505
+ if (!row) throw new Error("Self organization invitation lookup returned no result");
506
+ return OrganizationInvitation.parse(row.result);
507
+ });
508
+ }
509
+
510
+ export async function listOrganizationMembers(
511
+ db: Database,
512
+ input: { organizationId: string; actorSubjectId: string },
513
+ ): Promise<OrganizationMemberType[]> {
514
+ return await withRlsContext(
515
+ db,
516
+ { accountId: input.organizationId, workspaceId: null },
517
+ async (scopedDb) => {
518
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
519
+ const [row] = await rawRows<{ result: unknown }>(
520
+ scopedDb,
521
+ sql`select list_organization_members(
522
+ ${input.organizationId}::uuid,
523
+ ${input.actorSubjectId}
524
+ ) as result`,
525
+ );
526
+ return ListOrganizationMembersResponse.parse({
527
+ members: row?.result ?? [],
528
+ }).members;
529
+ },
530
+ );
531
+ }
532
+
533
+ export async function getOrganizationAdministrationOverview(
534
+ db: Database,
535
+ input: { organizationId: string; actorSubjectId: string },
536
+ ): Promise<OrganizationAdministrationOverviewType> {
537
+ return await withRlsContext(
538
+ db,
539
+ { accountId: input.organizationId, workspaceId: null },
540
+ async (scopedDb) => {
541
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
542
+ const [row] = await rawRows<{ result: unknown }>(
543
+ scopedDb,
544
+ sql`select get_organization_administration_overview(
545
+ ${input.organizationId}::uuid,
546
+ ${input.actorSubjectId}
547
+ ) as result`,
548
+ );
549
+ return OrganizationAdministrationOverview.parse(row?.result);
550
+ },
551
+ );
552
+ }
553
+
554
+ export async function updateOrganizationName(
555
+ db: Database,
556
+ input: {
557
+ organizationId: string;
558
+ actorSubjectId: string;
559
+ name: string;
560
+ expectedUpdatedAt: string;
561
+ operationId: string;
562
+ },
563
+ ): Promise<OrganizationSummaryType> {
564
+ return await withRlsContext(
565
+ db,
566
+ { accountId: input.organizationId, workspaceId: null },
567
+ async (scopedDb) => {
568
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
569
+ const [row] = await rawRows<{ result: unknown }>(
570
+ scopedDb,
571
+ sql`select update_organization_name(
572
+ ${input.organizationId}::uuid,
573
+ ${input.actorSubjectId},
574
+ ${input.name},
575
+ ${input.expectedUpdatedAt}::timestamptz,
576
+ ${input.operationId}::uuid
577
+ ) as result`,
578
+ );
579
+ return OrganizationSummary.parse(row?.result);
580
+ },
581
+ );
582
+ }
583
+
584
+ export async function getOrganizationPrivateSessionSettings(
585
+ db: Database,
586
+ input: { organizationId: string; actorSubjectId: string },
587
+ ): Promise<OrganizationPrivateSessionSettingsType> {
588
+ return await withRlsContext(
589
+ db,
590
+ { accountId: input.organizationId, workspaceId: null },
591
+ async (scopedDb) => {
592
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
593
+ const [row] = await rawRows<{ result: unknown }>(
594
+ scopedDb,
595
+ sql`select get_organization_private_session_settings(
596
+ ${input.organizationId}::uuid,
597
+ ${input.actorSubjectId}
598
+ ) as result`,
599
+ );
600
+ return OrganizationPrivateSessionSettings.parse(row?.result);
601
+ },
602
+ );
603
+ }
604
+
605
+ export async function updateOrganizationPrivateSessionSettings(
606
+ db: Database,
607
+ input: {
608
+ organizationId: string;
609
+ actorSubjectId: string;
610
+ enabled: boolean;
611
+ expectedVersion: number;
612
+ operationId: string;
613
+ },
614
+ ): Promise<OrganizationPrivateSessionSettingsType> {
615
+ return await withRlsContext(
616
+ db,
617
+ { accountId: input.organizationId, workspaceId: null },
618
+ async (scopedDb) => {
619
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
620
+ const [row] = await rawRows<{ result: unknown }>(
621
+ scopedDb,
622
+ sql`select update_organization_private_session_settings(
623
+ ${input.organizationId}::uuid,
624
+ ${input.actorSubjectId},
625
+ ${input.enabled},
626
+ ${input.expectedVersion}::bigint,
627
+ ${input.operationId}::uuid
628
+ ) as result`,
629
+ );
630
+ return OrganizationPrivateSessionSettings.parse(row?.result);
631
+ },
632
+ );
633
+ }
634
+
635
+ export async function listOrganizationInvitations(
636
+ db: Database,
637
+ input: {
638
+ organizationId: string;
639
+ actorSubjectId: string;
640
+ cursor?: string;
641
+ limit: number;
642
+ },
643
+ ): Promise<{
644
+ invitations: OrganizationInvitationType[];
645
+ nextCursor: string | null;
646
+ }> {
647
+ return await withRlsContext(
648
+ db,
649
+ { accountId: input.organizationId, workspaceId: null },
650
+ async (scopedDb) => {
651
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
652
+ const [row] = await rawRows<{ result: unknown }>(
653
+ scopedDb,
654
+ sql`select list_organization_invitations(
655
+ ${input.organizationId}::uuid,
656
+ ${input.actorSubjectId},
657
+ ${input.cursor ?? null}::uuid,
658
+ ${input.limit}
659
+ ) as result`,
660
+ );
661
+ const candidates = ListOrganizationInvitationsResponse.parse({
662
+ invitations: row?.result ?? [],
663
+ }).invitations;
664
+ const hasMore = candidates.length > input.limit;
665
+ const invitations = candidates.slice(0, input.limit);
666
+ return ListOrganizationInvitationsPageResponse.parse({
667
+ invitations,
668
+ nextCursor: hasMore ? (invitations.at(-1)?.id ?? null) : null,
669
+ });
670
+ },
671
+ );
672
+ }
673
+
674
+ export async function createOrganizationInvitation(
675
+ db: Database,
676
+ input: CommandBase & {
677
+ targetSubjectId: string | null;
678
+ targetEmail: string;
679
+ targetName?: string;
680
+ initialWorkspaceIds?: string[];
681
+ role: OrganizationMembershipRole;
682
+ expiresAt: string;
683
+ },
684
+ ): Promise<OrganizationInvitationType> {
685
+ const command = { action: "invite", ...input };
686
+ const result = await withRlsContext(
687
+ db,
688
+ { accountId: input.organizationId, workspaceId: null },
689
+ async (scopedDb) => {
690
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
691
+ const [row] = await rawRows<{ result: unknown }>(
692
+ scopedDb,
693
+ sql`select create_organization_invitation_v2(
694
+ ${JSON.stringify(command)}::jsonb
695
+ ) as result`,
696
+ );
697
+ if (!row) throw new Error("Organization invitation command returned no result");
698
+ return row.result;
699
+ },
700
+ );
701
+ return OrganizationInvitation.parse(result);
702
+ }
703
+
704
+ export async function bindPendingOrganizationInvitationsForVerifiedEmail(
705
+ db: Database,
706
+ input: { subjectId: string; email: string },
707
+ ): Promise<number> {
708
+ return await db.transaction(async (tx) => {
709
+ const txDb = tx as unknown as Database;
710
+ await setSubjectRlsContext(txDb, input.subjectId);
711
+ const [row] = await rawRows<{ result: number | string }>(
712
+ txDb,
713
+ sql`select bind_pending_organization_invitations_for_verified_email(
714
+ ${input.subjectId}::text, ${input.email}::text
715
+ ) as result`,
716
+ );
717
+ return Number(row?.result ?? 0);
718
+ });
719
+ }
720
+
721
+ export async function acceptOrganizationInvitation(
722
+ db: Database,
723
+ input: CommandBase & { invitationId: string; expectedRevision: number },
724
+ ): Promise<{
725
+ invitation: OrganizationInvitationType;
726
+ membership: OrganizationMemberType;
727
+ }> {
728
+ // `accept` inserts the invited human's personal workspace, so like
729
+ // suspend/offboard it acquires workspace rows and can be inside a lock cycle
730
+ // with an ordinary workspace writer. It needs the identical replay.
731
+ const command = { action: "accept", ...input };
732
+ const result = await withOrganizationLifecycleDeadlockReplay(async () =>
733
+ withRlsContext(db, { accountId: input.organizationId, workspaceId: null }, async (scopedDb) => {
734
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
735
+ const [row] = await rawRows<{ result: unknown }>(
736
+ scopedDb,
737
+ sql`select accept_organization_invitation_v2(
738
+ ${JSON.stringify(command)}::jsonb
739
+ ) as result`,
740
+ );
741
+ if (!row) throw new Error("Organization invitation acceptance returned no result");
742
+ return row.result;
743
+ }),
744
+ );
745
+ return {
746
+ invitation: OrganizationInvitation.parse((result as { invitation?: unknown }).invitation),
747
+ membership: OrganizationMember.parse((result as { membership?: unknown }).membership),
748
+ };
749
+ }
750
+
751
+ export async function revokeOrganizationInvitation(
752
+ db: Database,
753
+ input: CommandBase & { invitationId: string; expectedRevision: number },
754
+ ): Promise<OrganizationInvitationType> {
755
+ return OrganizationInvitation.parse(
756
+ await runCommand(db, { action: "revoke_invitation", ...input }),
757
+ );
758
+ }
759
+
760
+ export async function updateOrganizationMember(
761
+ db: Database,
762
+ input: CommandBase & {
763
+ membershipId: string;
764
+ transition: UpdateOrganizationMemberRequest;
765
+ },
766
+ ): Promise<OrganizationMemberType> {
767
+ const { transition, ...base } = input;
768
+ const command = {
769
+ action: transition.kind,
770
+ ...base,
771
+ expectedAuthorizationRevision: transition.expectedAuthorizationRevision,
772
+ ...(transition.role === undefined ? {} : { role: transition.role }),
773
+ ...(transition.reason === undefined ? {} : { reason: transition.reason }),
774
+ };
775
+ return await withOrganizationLifecycleDeadlockReplay(async () =>
776
+ db.transaction(async (tx) => {
777
+ if (transition.kind === "suspend" || transition.kind === "offboard") {
778
+ const settlements = await prepareOrganizationMembershipProtocolSettlements(
779
+ tx as unknown as Database,
780
+ command,
781
+ );
782
+ await settleOrganizationMembershipProtocols(tx as unknown as Database, settlements);
783
+ }
784
+ return OrganizationMember.parse(await runCommand(tx as unknown as Database, command));
785
+ }),
786
+ );
787
+ }
788
+
789
+ export async function getOrganizationRetentionPolicy(
790
+ db: Database,
791
+ input: { organizationId: string; actorSubjectId: string },
792
+ ): Promise<OrganizationRetentionPolicyType> {
793
+ return await withRlsContext(
794
+ db,
795
+ { accountId: input.organizationId, workspaceId: null },
796
+ async (scopedDb) => {
797
+ await setSubjectRlsContext(scopedDb, input.actorSubjectId);
798
+ const [row] = await rawRows<{ result: unknown }>(
799
+ scopedDb,
800
+ sql`select get_organization_retention_policy(
801
+ ${input.organizationId}::uuid,
802
+ ${input.actorSubjectId}
803
+ ) as result`,
804
+ );
805
+ return OrganizationRetentionPolicy.parse(row?.result);
806
+ },
807
+ );
808
+ }
809
+
810
+ export async function updateOrganizationRetentionPolicy(
811
+ db: Database,
812
+ input: CommandBase & {
813
+ mode: "retain" | "delete_after";
814
+ retentionDays: number | null;
815
+ expectedVersion: number;
816
+ },
817
+ ): Promise<OrganizationRetentionPolicyType> {
818
+ return OrganizationRetentionPolicy.parse(await runCommand(db, { action: "retention", ...input }));
819
+ }
820
+
821
+ async function runRetentionCapability<T>(
822
+ db: Database,
823
+ organizationId: string,
824
+ query: (scopedDb: Database) => Promise<T>,
825
+ ): Promise<T> {
826
+ return await withRlsContext(
827
+ db,
828
+ { accountId: organizationId, workspaceId: null },
829
+ async (scopedDb) => await query(scopedDb),
830
+ );
831
+ }
832
+
833
+ export async function previewOrganizationRetentionDeletions(
834
+ db: Database,
835
+ input: { organizationId: string; limit?: number },
836
+ ): Promise<OrganizationRetentionDeletionPreviewType[]> {
837
+ return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
838
+ const [row] = await rawRows<{ result: unknown }>(
839
+ scopedDb,
840
+ sql`select preview_organization_retention_deletions(
841
+ ${input.organizationId}::uuid, ${input.limit ?? 25}
842
+ ) as result`,
843
+ );
844
+ return OrganizationRetentionDeletionPreview.array().parse(row?.result ?? []);
845
+ });
846
+ }
847
+
848
+ export async function claimOrganizationRetentionDeletion(
849
+ db: Database,
850
+ input: { organizationId: string; operationId: string; excludedMembershipIds?: string[] },
851
+ ): Promise<OrganizationRetentionDeletionClaimType | null> {
852
+ return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
853
+ const excludedMembershipIds = input.excludedMembershipIds ?? [];
854
+ const excludedMembershipArray =
855
+ excludedMembershipIds.length === 0
856
+ ? sql`ARRAY[]::uuid[]`
857
+ : sql`ARRAY[${sql.join(
858
+ excludedMembershipIds.map((membershipId) => sql`${membershipId}::uuid`),
859
+ sql`, `,
860
+ )}]::uuid[]`;
861
+ const [row] = await rawRows<{ result: unknown }>(
862
+ scopedDb,
863
+ sql`select claim_organization_retention_deletion(
864
+ ${input.organizationId}::uuid, ${input.operationId}::uuid,
865
+ ${excludedMembershipArray}
866
+ ) as result`,
867
+ );
868
+ return row?.result === null || row?.result === undefined
869
+ ? null
870
+ : OrganizationRetentionDeletionClaim.parse(row.result);
871
+ });
872
+ }
873
+
874
+ export async function listOrganizationRetentionDeletionObjects(
875
+ db: Database,
876
+ input: {
877
+ organizationId: string;
878
+ membershipId: string;
879
+ operationId: string;
880
+ objectBucket: string;
881
+ limit?: number;
882
+ },
883
+ ): Promise<OrganizationRetentionDeletionObjectType[]> {
884
+ return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
885
+ const [row] = await rawRows<{ result: unknown }>(
886
+ scopedDb,
887
+ sql`select list_organization_retention_deletion_objects(
888
+ ${input.organizationId}::uuid,
889
+ ${input.membershipId}::uuid,
890
+ ${input.operationId}::uuid,
891
+ ${input.objectBucket},
892
+ ${input.limit ?? 100}
893
+ ) as result`,
894
+ );
895
+ return OrganizationRetentionDeletionObject.array().parse(row?.result ?? []);
896
+ });
897
+ }
898
+
899
+ export async function recordOrganizationRetentionObjectDeleted(
900
+ db: Database,
901
+ input: {
902
+ organizationId: string;
903
+ membershipId: string;
904
+ operationId: string;
905
+ objectKind: OrganizationRetentionDeletionObjectType["objectKind"];
906
+ sourceId: string;
907
+ objectBucket: string;
908
+ objectKey: string;
909
+ },
910
+ ): Promise<boolean> {
911
+ return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
912
+ const [row] = await rawRows<{ result: boolean }>(
913
+ scopedDb,
914
+ sql`select record_organization_retention_object_deleted(
915
+ ${input.organizationId}::uuid,
916
+ ${input.membershipId}::uuid,
917
+ ${input.operationId}::uuid,
918
+ ${input.objectKind},
919
+ ${input.sourceId},
920
+ ${input.objectBucket},
921
+ ${input.objectKey}
922
+ ) as result`,
923
+ );
924
+ return row?.result === true;
925
+ });
926
+ }
927
+
928
+ export async function failOrganizationRetentionDeletion(
929
+ db: Database,
930
+ input: {
931
+ organizationId: string;
932
+ membershipId: string;
933
+ operationId: string;
934
+ reasonCode: string;
935
+ },
936
+ ): Promise<boolean> {
937
+ return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
938
+ const [row] = await rawRows<{ result: boolean }>(
939
+ scopedDb,
940
+ sql`select fail_organization_retention_deletion(
941
+ ${input.organizationId}::uuid,
942
+ ${input.membershipId}::uuid,
943
+ ${input.operationId}::uuid,
944
+ ${input.reasonCode}
945
+ ) as result`,
946
+ );
947
+ return row?.result === true;
948
+ });
949
+ }
950
+
951
+ export async function finalizeOrganizationRetentionDeletion(
952
+ db: Database,
953
+ input: {
954
+ organizationId: string;
955
+ membershipId: string;
956
+ operationId: string;
957
+ objectBucket: string;
958
+ },
959
+ ): Promise<OrganizationRetentionDatabaseFinalizationType> {
960
+ return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
961
+ const [row] = await rawRows<{ result: unknown }>(
962
+ scopedDb,
963
+ sql`select finalize_organization_retention_deletion(
964
+ ${input.organizationId}::uuid,
965
+ ${input.membershipId}::uuid,
966
+ ${input.operationId}::uuid,
967
+ ${input.objectBucket}
968
+ ) as result`,
969
+ );
970
+ return OrganizationRetentionDatabaseFinalization.parse(row?.result);
971
+ });
972
+ }
973
+
974
+ export async function completeOrganizationRetentionDeletion(
975
+ db: Database,
976
+ input: {
977
+ organizationId: string;
978
+ membershipId: string;
979
+ operationId: string;
980
+ objectBucket: string;
981
+ },
982
+ ): Promise<OrganizationRetentionDeletionResultType> {
983
+ return await runRetentionCapability(db, input.organizationId, async (scopedDb) => {
984
+ const [row] = await rawRows<{ result: unknown }>(
985
+ scopedDb,
986
+ sql`select complete_organization_retention_deletion(
987
+ ${input.organizationId}::uuid,
988
+ ${input.membershipId}::uuid,
989
+ ${input.operationId}::uuid,
990
+ ${input.objectBucket}
991
+ ) as result`,
992
+ );
993
+ return OrganizationRetentionDeletionResult.parse(row?.result);
994
+ });
995
+ }