@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,606 @@
1
+ -- deployment-mode: maintenance
2
+ -- Permit an organization invitation to exist before its target has an
3
+ -- OpenGeni login. A verified Better Auth email binds the invitation to the
4
+ -- canonical user subject before the existing acceptance lifecycle runs.
5
+ -- This is a one-way application protocol cutover: stop every API, control
6
+ -- worker, and turn worker before applying it, and never restart a pre-0314
7
+ -- image. Old callers can accept without initial workspace grants and can
8
+ -- provision a fallback organization before verified invitation convergence.
9
+
10
+ SET LOCAL lock_timeout = '5s';
11
+ SET LOCAL statement_timeout = '5min';
12
+
13
+ DO $organization_invitation_writer_drain_before_lock$
14
+ DECLARE
15
+ configured_roles_text text := nullif(
16
+ current_setting('opengeni.migration_application_roles', true), ''
17
+ );
18
+ configured_roles jsonb;
19
+ BEGIN
20
+ IF configured_roles_text IS NULL THEN
21
+ RAISE EXCEPTION
22
+ '0314 organization invitation activation requires an explicit application database role list'
23
+ USING ERRCODE = '55000';
24
+ END IF;
25
+ BEGIN
26
+ configured_roles := configured_roles_text::jsonb;
27
+ EXCEPTION WHEN OTHERS THEN
28
+ RAISE EXCEPTION
29
+ '0314 organization invitation activation received a malformed application database role list'
30
+ USING ERRCODE = '55000';
31
+ END;
32
+ IF jsonb_typeof(configured_roles) <> 'array'
33
+ OR jsonb_array_length(configured_roles) NOT BETWEEN 1 AND 16
34
+ OR EXISTS (
35
+ SELECT 1 FROM jsonb_array_elements(configured_roles) AS roles(value)
36
+ WHERE jsonb_typeof(value) <> 'string'
37
+ OR btrim(value #>> '{}') = ''
38
+ OR octet_length(value #>> '{}') > 63
39
+ )
40
+ OR (
41
+ SELECT count(*) FROM jsonb_array_elements_text(configured_roles)
42
+ ) <> (
43
+ SELECT count(DISTINCT value)
44
+ FROM jsonb_array_elements_text(configured_roles) AS roles(value)
45
+ )
46
+ THEN
47
+ RAISE EXCEPTION
48
+ '0314 organization invitation activation received an invalid application database role list'
49
+ USING ERRCODE = '55000';
50
+ END IF;
51
+ IF EXISTS (
52
+ SELECT 1 FROM pg_stat_activity activity
53
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
54
+ ON roles.role_name = activity.usename
55
+ WHERE activity.datname = current_database()
56
+ AND activity.pid <> pg_backend_pid()
57
+ )
58
+ THEN
59
+ RAISE EXCEPTION
60
+ '0314 organization invitation activation requires all configured OpenGeni application database sessions to be stopped'
61
+ USING ERRCODE = '55000';
62
+ END IF;
63
+ END
64
+ $organization_invitation_writer_drain_before_lock$;
65
+
66
+ LOCK TABLE organization_membership_invitations IN ACCESS EXCLUSIVE MODE;
67
+ LOCK TABLE organization_membership_operation_receipts IN ACCESS EXCLUSIVE MODE;
68
+ LOCK TABLE organization_memberships IN ACCESS EXCLUSIVE MODE;
69
+ LOCK TABLE workspaces IN ACCESS EXCLUSIVE MODE;
70
+ LOCK TABLE workspace_memberships IN ACCESS EXCLUSIVE MODE;
71
+
72
+ DO $organization_invitation_writer_drain_after_lock$
73
+ DECLARE
74
+ configured_roles jsonb := current_setting(
75
+ 'opengeni.migration_application_roles', false
76
+ )::jsonb;
77
+ BEGIN
78
+ IF EXISTS (
79
+ SELECT 1 FROM pg_stat_activity activity
80
+ JOIN jsonb_array_elements_text(configured_roles) roles(role_name)
81
+ ON roles.role_name = activity.usename
82
+ WHERE activity.datname = current_database()
83
+ AND activity.pid <> pg_backend_pid()
84
+ )
85
+ THEN
86
+ RAISE EXCEPTION
87
+ '0314 organization invitation activation requires all configured OpenGeni application database sessions to be stopped'
88
+ USING ERRCODE = '55000';
89
+ END IF;
90
+ END
91
+ $organization_invitation_writer_drain_after_lock$;
92
+
93
+ ALTER TABLE organization_membership_invitations
94
+ ALTER COLUMN target_subject_id DROP NOT NULL,
95
+ ADD COLUMN target_name text,
96
+ ADD COLUMN initial_workspace_ids uuid[] NOT NULL DEFAULT '{}'::uuid[];
97
+
98
+ ALTER TABLE organization_membership_invitations
99
+ DROP CONSTRAINT organization_membership_invitations_subject_check,
100
+ ADD CONSTRAINT organization_membership_invitations_subject_check CHECK (
101
+ target_subject_id IS NULL
102
+ OR (
103
+ target_subject_id = btrim(target_subject_id)
104
+ AND target_subject_id LIKE 'user:%'
105
+ AND octet_length(convert_to(target_subject_id, 'UTF8')) BETWEEN 6 AND 1024
106
+ )
107
+ ),
108
+ ADD CONSTRAINT organization_membership_invitations_name_check CHECK (
109
+ target_name IS NULL
110
+ OR (
111
+ target_name = btrim(target_name)
112
+ AND octet_length(convert_to(target_name, 'UTF8')) BETWEEN 1 AND 120
113
+ )
114
+ ),
115
+ ADD CONSTRAINT organization_membership_invitations_workspace_count_check CHECK (
116
+ cardinality(initial_workspace_ids) BETWEEN 0 AND 100
117
+ );
118
+
119
+ DROP INDEX organization_membership_invitations_pending_target_uq;
120
+ CREATE UNIQUE INDEX organization_membership_invitations_pending_target_uq
121
+ ON organization_membership_invitations (account_id, target_subject_id)
122
+ WHERE status = 'pending' AND target_subject_id IS NOT NULL;
123
+ CREATE UNIQUE INDEX organization_membership_invitations_pending_email_uq
124
+ ON organization_membership_invitations (account_id, target_email)
125
+ WHERE status = 'pending';
126
+
127
+ CREATE TABLE organization_invitation_binding_events (
128
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
129
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE CASCADE,
130
+ invitation_id uuid NOT NULL,
131
+ target_subject_id text NOT NULL,
132
+ resulting_revision bigint NOT NULL,
133
+ bound_at timestamptz NOT NULL DEFAULT clock_timestamp(),
134
+ CONSTRAINT organization_invitation_binding_events_invitation_fk
135
+ FOREIGN KEY (invitation_id, account_id)
136
+ REFERENCES organization_membership_invitations(id, account_id) ON DELETE RESTRICT,
137
+ CONSTRAINT organization_invitation_binding_events_subject_check CHECK (
138
+ target_subject_id = btrim(target_subject_id)
139
+ AND target_subject_id LIKE 'user:%'
140
+ AND octet_length(convert_to(target_subject_id, 'UTF8')) BETWEEN 6 AND 1024
141
+ ),
142
+ CONSTRAINT organization_invitation_binding_events_revision_check CHECK (
143
+ resulting_revision > 1
144
+ )
145
+ );
146
+ CREATE UNIQUE INDEX organization_invitation_binding_events_invitation_uq
147
+ ON organization_invitation_binding_events (account_id, invitation_id);
148
+
149
+ ALTER TABLE organization_invitation_binding_events ENABLE ROW LEVEL SECURITY;
150
+ ALTER TABLE organization_invitation_binding_events FORCE ROW LEVEL SECURITY;
151
+ CREATE POLICY organization_tenancy_lifecycle ON organization_invitation_binding_events
152
+ USING (current_setting('opengeni.organization_tenancy_lifecycle', true)
153
+ = 'organization_membership_lifecycle')
154
+ WITH CHECK (current_setting('opengeni.organization_tenancy_lifecycle', true)
155
+ = 'organization_membership_lifecycle');
156
+
157
+ CREATE OR REPLACE FUNCTION opengeni_private.organization_invitation_row_json(
158
+ p_invitation organization_membership_invitations
159
+ ) RETURNS jsonb
160
+ LANGUAGE sql STABLE
161
+ SET search_path = pg_catalog
162
+ AS $body$
163
+ SELECT pg_catalog.jsonb_build_object(
164
+ 'id', p_invitation.id,
165
+ 'organizationId', p_invitation.account_id,
166
+ 'targetEmail', p_invitation.target_email,
167
+ 'targetName', p_invitation.target_name,
168
+ 'initialWorkspaceIds', p_invitation.initial_workspace_ids,
169
+ 'role', p_invitation.role,
170
+ 'status', CASE
171
+ WHEN p_invitation.status = 'pending'
172
+ AND p_invitation.expires_at <= pg_catalog.clock_timestamp()
173
+ THEN 'expired'
174
+ ELSE p_invitation.status
175
+ END,
176
+ 'revision', p_invitation.revision,
177
+ 'expiresAt', p_invitation.expires_at,
178
+ 'acceptedMembershipId', p_invitation.accepted_membership_id,
179
+ 'createdAt', p_invitation.created_at,
180
+ 'updatedAt', p_invitation.updated_at
181
+ )
182
+ $body$;
183
+
184
+ CREATE FUNCTION create_organization_invitation_v2(p_command jsonb)
185
+ RETURNS jsonb
186
+ LANGUAGE plpgsql
187
+ SECURITY DEFINER
188
+ SET search_path FROM CURRENT
189
+ AS $body$
190
+ DECLARE
191
+ account_id_value uuid := nullif(p_command ->> 'organizationId', '')::uuid;
192
+ actor_subject text := p_command ->> 'actorSubjectId';
193
+ operation_id_value uuid := nullif(p_command ->> 'operationId', '')::uuid;
194
+ input_hash_value text;
195
+ receipt_row organization_membership_operation_receipts%ROWTYPE;
196
+ actor organization_memberships%ROWTYPE;
197
+ invitation organization_membership_invitations%ROWTYPE;
198
+ target_subject text := nullif(p_command ->> 'targetSubjectId', '');
199
+ target_email_value text := lower(btrim(p_command ->> 'targetEmail'));
200
+ target_name_value text := nullif(btrim(p_command ->> 'targetName'), '');
201
+ requested_role text := p_command ->> 'role';
202
+ expires_at_value timestamptz := nullif(p_command ->> 'expiresAt', '')::timestamptz;
203
+ requested_workspace_ids uuid[];
204
+ now_value timestamptz := pg_catalog.clock_timestamp();
205
+ result jsonb;
206
+ BEGIN
207
+ IF p_command IS NULL
208
+ OR account_id_value IS NULL
209
+ OR actor_subject IS NULL
210
+ OR actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
211
+ OR account_id_value IS DISTINCT FROM opengeni_private.current_account_id()
212
+ OR operation_id_value IS NULL
213
+ THEN
214
+ RAISE EXCEPTION 'organization invitation authority is invalid' USING ERRCODE = '42501';
215
+ END IF;
216
+
217
+ SELECT coalesce(pg_catalog.array_agg(candidate.workspace_id ORDER BY candidate.workspace_id), '{}')
218
+ INTO requested_workspace_ids
219
+ FROM (
220
+ SELECT DISTINCT value::uuid AS workspace_id
221
+ FROM pg_catalog.jsonb_array_elements_text(
222
+ coalesce(p_command -> 'initialWorkspaceIds', '[]'::jsonb)
223
+ ) value
224
+ ) candidate;
225
+
226
+ IF target_email_value IS NULL
227
+ OR target_email_value <> btrim(target_email_value)
228
+ OR octet_length(convert_to(target_email_value, 'UTF8')) NOT BETWEEN 3 AND 320
229
+ OR (target_subject IS NOT NULL AND target_subject NOT LIKE 'user:%')
230
+ OR (target_name_value IS NOT NULL
231
+ AND octet_length(convert_to(target_name_value, 'UTF8')) NOT BETWEEN 1 AND 120)
232
+ OR requested_role NOT IN ('owner', 'admin', 'member')
233
+ OR expires_at_value <= now_value
234
+ OR expires_at_value > now_value + interval '30 days'
235
+ OR cardinality(requested_workspace_ids) > 100
236
+ THEN
237
+ RAISE EXCEPTION 'organization invitation input is invalid' USING ERRCODE = '22023';
238
+ END IF;
239
+
240
+ -- Email-first is the canonical ordering shared with verified signup
241
+ -- convergence. It makes invitation creation and fallback organization
242
+ -- provisioning mutually exclusive for one normalized identity.
243
+ PERFORM pg_catalog.pg_advisory_xact_lock(
244
+ pg_catalog.hashtextextended(
245
+ 'organization-invitation-email:' || target_email_value, 0
246
+ )
247
+ );
248
+ PERFORM pg_catalog.pg_advisory_xact_lock(
249
+ pg_catalog.hashtextextended('organization-membership:' || account_id_value::text, 0)
250
+ );
251
+ PERFORM 1 FROM managed_accounts account
252
+ WHERE account.id = account_id_value FOR KEY SHARE;
253
+ IF NOT FOUND THEN
254
+ RAISE EXCEPTION 'organization not found' USING ERRCODE = 'P0002';
255
+ END IF;
256
+ PERFORM pg_catalog.set_config(
257
+ 'opengeni.organization_tenancy_lifecycle',
258
+ 'organization_membership_lifecycle', true
259
+ );
260
+
261
+ input_hash_value := pg_catalog.encode(
262
+ pg_catalog.sha256(pg_catalog.convert_to(p_command::text, 'UTF8')), 'hex'
263
+ );
264
+ SELECT * INTO receipt_row
265
+ FROM organization_membership_operation_receipts receipt
266
+ WHERE receipt.account_id = account_id_value
267
+ AND receipt.operation_id = operation_id_value
268
+ FOR UPDATE;
269
+ IF FOUND THEN
270
+ IF receipt_row.action IS DISTINCT FROM 'invite'
271
+ OR receipt_row.input_hash IS DISTINCT FROM input_hash_value
272
+ THEN
273
+ RAISE EXCEPTION 'organization operation id was reused with different input'
274
+ USING ERRCODE = '23505';
275
+ END IF;
276
+ RETURN receipt_row.result;
277
+ END IF;
278
+
279
+ SELECT * INTO actor FROM organization_memberships membership
280
+ WHERE membership.account_id = account_id_value
281
+ AND membership.subject_id = actor_subject
282
+ FOR UPDATE;
283
+ IF NOT FOUND OR actor.status <> 'active' OR actor.role NOT IN ('owner', 'admin') THEN
284
+ RAISE EXCEPTION 'organization administration required' USING ERRCODE = '42501';
285
+ END IF;
286
+ IF actor.role = 'admin' AND requested_role <> 'member' THEN
287
+ RAISE EXCEPTION 'administrators may invite members only' USING ERRCODE = '22023';
288
+ END IF;
289
+
290
+ IF cardinality(requested_workspace_ids) > 0 THEN
291
+ IF (
292
+ SELECT pg_catalog.count(*)::integer
293
+ FROM workspaces workspace
294
+ WHERE workspace.account_id = account_id_value
295
+ AND workspace.id = ANY(requested_workspace_ids)
296
+ AND NOT EXISTS (
297
+ SELECT 1 FROM organization_memberships membership
298
+ WHERE membership.account_id = account_id_value
299
+ AND membership.personal_workspace_id = workspace.id
300
+ )
301
+ ) IS DISTINCT FROM cardinality(requested_workspace_ids)
302
+ THEN
303
+ RAISE EXCEPTION 'initial workspace access contains an unavailable workspace'
304
+ USING ERRCODE = '22023';
305
+ END IF;
306
+ END IF;
307
+
308
+ IF target_subject IS NOT NULL AND EXISTS (
309
+ SELECT 1 FROM organization_memberships membership
310
+ WHERE membership.account_id = account_id_value
311
+ AND membership.subject_id = target_subject
312
+ AND membership.status = 'active'
313
+ ) THEN
314
+ RAISE EXCEPTION 'subject is already an active organization member' USING ERRCODE = '55000';
315
+ END IF;
316
+ IF target_subject IS NOT NULL AND EXISTS (
317
+ SELECT 1 FROM organization_memberships membership
318
+ WHERE membership.account_id = account_id_value
319
+ AND membership.subject_id = target_subject
320
+ AND membership.status IN ('suspended', 'revoked')
321
+ ) THEN
322
+ RAISE EXCEPTION 'inactive organization membership cannot be re-invited'
323
+ USING ERRCODE = '55000';
324
+ END IF;
325
+
326
+ UPDATE organization_membership_invitations SET
327
+ status = 'revoked', revision = revision + 1, updated_at = now_value
328
+ WHERE account_id = account_id_value
329
+ AND status = 'pending'
330
+ AND (
331
+ organization_membership_invitations.target_email = target_email_value
332
+ OR (target_subject IS NOT NULL AND target_subject_id = target_subject)
333
+ );
334
+
335
+ INSERT INTO organization_membership_invitations (
336
+ account_id, target_subject_id, target_email, target_name,
337
+ initial_workspace_ids, role, created_by_membership_id, expires_at
338
+ ) VALUES (
339
+ account_id_value, target_subject, target_email_value, target_name_value,
340
+ requested_workspace_ids, requested_role, actor.id, expires_at_value
341
+ ) RETURNING * INTO invitation;
342
+
343
+ result := opengeni_private.organization_invitation_row_json(invitation);
344
+ INSERT INTO organization_membership_operation_receipts (
345
+ account_id, operation_id, action, input_hash, result
346
+ ) VALUES (
347
+ account_id_value, operation_id_value, 'invite', input_hash_value, result
348
+ );
349
+ INSERT INTO organization_membership_lifecycle_events (
350
+ account_id, operation_id, actor_membership_id, target_membership_id, kind,
351
+ prior_authorization_revision, resulting_authorization_revision, reason
352
+ ) VALUES (
353
+ account_id_value, operation_id_value, actor.id, NULL, 'invite', NULL, NULL, NULL
354
+ );
355
+ RETURN result;
356
+ END
357
+ $body$;
358
+
359
+ CREATE FUNCTION bind_pending_organization_invitations_for_verified_email(
360
+ p_subject_id text,
361
+ p_email text
362
+ ) RETURNS integer
363
+ LANGUAGE plpgsql
364
+ SECURITY DEFINER
365
+ SET search_path FROM CURRENT
366
+ AS $body$
367
+ DECLARE
368
+ normalized_email text := lower(btrim(p_email));
369
+ user_id_value text;
370
+ bound_count integer := 0;
371
+ account_id_value uuid;
372
+ BEGIN
373
+ IF p_subject_id IS NULL
374
+ OR p_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
375
+ OR p_subject_id NOT LIKE 'user:%'
376
+ OR normalized_email IS NULL
377
+ OR octet_length(convert_to(normalized_email, 'UTF8')) NOT BETWEEN 3 AND 320
378
+ THEN
379
+ RAISE EXCEPTION 'verified managed human authority required' USING ERRCODE = '42501';
380
+ END IF;
381
+ user_id_value := substring(p_subject_id FROM 6);
382
+ IF user_id_value IS NULL
383
+ OR octet_length(convert_to(user_id_value, 'UTF8')) NOT BETWEEN 1 AND 1019
384
+ THEN
385
+ RAISE EXCEPTION 'verified managed human authority required' USING ERRCODE = '42501';
386
+ END IF;
387
+ IF NOT EXISTS (
388
+ SELECT 1 FROM auth_users auth_user
389
+ WHERE auth_user.id = user_id_value
390
+ AND auth_user.email_verified IS TRUE
391
+ AND lower(btrim(auth_user.email)) = normalized_email
392
+ ) THEN
393
+ RAISE EXCEPTION 'verified managed human authority required' USING ERRCODE = '42501';
394
+ END IF;
395
+
396
+ -- Hold this transaction-scoped fence through the caller's subsequent
397
+ -- pending-invitation check and fallback provisioning decision. Creation
398
+ -- takes the same fence before any organization lock, so neither path can
399
+ -- snapshot an absence while a matching invitation is committing.
400
+ PERFORM pg_catalog.pg_advisory_xact_lock(
401
+ pg_catalog.hashtextextended(
402
+ 'organization-invitation-email:' || normalized_email, 0
403
+ )
404
+ );
405
+ PERFORM pg_catalog.set_config(
406
+ 'opengeni.organization_tenancy_lifecycle',
407
+ 'organization_membership_lifecycle', true
408
+ );
409
+ FOR account_id_value IN
410
+ SELECT DISTINCT invitation.account_id
411
+ FROM organization_membership_invitations invitation
412
+ WHERE invitation.target_subject_id IS NULL
413
+ AND invitation.target_email = normalized_email
414
+ AND invitation.status = 'pending'
415
+ AND invitation.expires_at > pg_catalog.clock_timestamp()
416
+ ORDER BY invitation.account_id
417
+ LOOP
418
+ PERFORM pg_catalog.pg_advisory_xact_lock(
419
+ pg_catalog.hashtextextended(
420
+ 'organization-membership:' || account_id_value::text, 0
421
+ )
422
+ );
423
+ PERFORM 1 FROM managed_accounts account
424
+ WHERE account.id = account_id_value FOR KEY SHARE;
425
+ WITH bound AS (
426
+ UPDATE organization_membership_invitations invitation SET
427
+ target_subject_id = p_subject_id,
428
+ revision = invitation.revision + 1,
429
+ updated_at = pg_catalog.clock_timestamp()
430
+ WHERE invitation.account_id = account_id_value
431
+ AND invitation.target_subject_id IS NULL
432
+ AND invitation.target_email = normalized_email
433
+ AND invitation.status = 'pending'
434
+ AND invitation.expires_at > pg_catalog.clock_timestamp()
435
+ AND NOT EXISTS (
436
+ SELECT 1 FROM organization_membership_invitations existing
437
+ WHERE existing.account_id = invitation.account_id
438
+ AND existing.target_subject_id = p_subject_id
439
+ AND existing.status = 'pending'
440
+ )
441
+ RETURNING invitation.id, invitation.account_id, invitation.revision
442
+ ), recorded AS (
443
+ INSERT INTO organization_invitation_binding_events (
444
+ account_id, invitation_id, target_subject_id, resulting_revision
445
+ )
446
+ SELECT account_id, id, p_subject_id, revision FROM bound
447
+ RETURNING 1
448
+ )
449
+ SELECT bound_count + pg_catalog.count(*)::integer
450
+ INTO bound_count FROM recorded;
451
+ END LOOP;
452
+ RETURN bound_count;
453
+ END
454
+ $body$;
455
+
456
+ CREATE FUNCTION has_pending_organization_invitation_for_subject(p_subject_id text)
457
+ RETURNS boolean
458
+ LANGUAGE plpgsql
459
+ SECURITY DEFINER
460
+ SET search_path FROM CURRENT
461
+ AS $body$
462
+ BEGIN
463
+ IF p_subject_id IS NULL
464
+ OR p_subject_id IS DISTINCT FROM opengeni_private.current_subject_id()
465
+ OR p_subject_id NOT LIKE 'user:%'
466
+ THEN
467
+ RAISE EXCEPTION 'managed human subject authority required' USING ERRCODE = '42501';
468
+ END IF;
469
+ PERFORM pg_catalog.set_config(
470
+ 'opengeni.organization_tenancy_lifecycle',
471
+ 'organization_membership_lifecycle', true
472
+ );
473
+ RETURN EXISTS (
474
+ SELECT 1 FROM organization_membership_invitations invitation
475
+ WHERE invitation.target_subject_id = p_subject_id
476
+ AND invitation.status = 'pending'
477
+ AND invitation.expires_at > pg_catalog.clock_timestamp()
478
+ );
479
+ END
480
+ $body$;
481
+
482
+ CREATE FUNCTION accept_organization_invitation_v2(p_command jsonb)
483
+ RETURNS jsonb
484
+ LANGUAGE plpgsql
485
+ SECURITY DEFINER
486
+ SET search_path FROM CURRENT
487
+ AS $body$
488
+ DECLARE
489
+ account_id_value uuid := nullif(p_command ->> 'organizationId', '')::uuid;
490
+ actor_subject text := p_command ->> 'actorSubjectId';
491
+ invitation_id_value uuid := nullif(p_command ->> 'invitationId', '')::uuid;
492
+ invitation organization_membership_invitations%ROWTYPE;
493
+ result jsonb;
494
+ default_member_permissions jsonb := '[
495
+ "workspace:read", "sessions:create", "sessions:read", "sessions:control",
496
+ "files:upload", "files:read", "documents:manage", "documents:search",
497
+ "scheduled_tasks:manage", "scheduled_tasks:run", "github:use",
498
+ "variable-sets:list", "variable-sets:read", "variable-sets:write",
499
+ "variable-sets:attach", "variable-sets:use", "secrets:list", "secrets:write",
500
+ "goals:manage"
501
+ ]'::jsonb;
502
+ BEGIN
503
+ IF account_id_value IS NULL
504
+ OR actor_subject IS NULL
505
+ OR actor_subject IS DISTINCT FROM opengeni_private.current_subject_id()
506
+ OR account_id_value IS DISTINCT FROM opengeni_private.current_account_id()
507
+ OR invitation_id_value IS NULL
508
+ THEN
509
+ RAISE EXCEPTION 'organization invitation acceptance authority is invalid'
510
+ USING ERRCODE = '42501';
511
+ END IF;
512
+ PERFORM pg_catalog.set_config(
513
+ 'opengeni.organization_tenancy_lifecycle',
514
+ 'organization_membership_lifecycle', true
515
+ );
516
+ result := organization_membership_command(p_command);
517
+
518
+ SELECT * INTO invitation
519
+ FROM organization_membership_invitations candidate
520
+ WHERE candidate.account_id = account_id_value
521
+ AND candidate.id = invitation_id_value;
522
+ IF NOT FOUND OR invitation.target_subject_id IS DISTINCT FROM actor_subject THEN
523
+ RAISE EXCEPTION 'organization invitation not found' USING ERRCODE = 'P0002';
524
+ END IF;
525
+
526
+ IF cardinality(invitation.initial_workspace_ids) > 0 THEN
527
+ IF (
528
+ SELECT pg_catalog.count(*)::integer
529
+ FROM workspaces workspace
530
+ WHERE workspace.account_id = account_id_value
531
+ AND workspace.id = ANY(invitation.initial_workspace_ids)
532
+ AND NOT EXISTS (
533
+ SELECT 1 FROM organization_memberships membership
534
+ WHERE membership.account_id = account_id_value
535
+ AND membership.personal_workspace_id = workspace.id
536
+ )
537
+ ) IS DISTINCT FROM cardinality(invitation.initial_workspace_ids)
538
+ THEN
539
+ RAISE EXCEPTION 'initial workspace access is no longer available'
540
+ USING ERRCODE = '55000';
541
+ END IF;
542
+ INSERT INTO workspace_memberships (
543
+ account_id, workspace_id, subject_id, subject_label, role, permissions
544
+ )
545
+ SELECT
546
+ account_id_value, workspace_id, actor_subject,
547
+ coalesce(invitation.target_name, invitation.target_email),
548
+ 'member', default_member_permissions
549
+ FROM pg_catalog.unnest(invitation.initial_workspace_ids) workspace_id
550
+ ON CONFLICT (subject_id, workspace_id) DO NOTHING;
551
+ END IF;
552
+ RETURN result;
553
+ END
554
+ $body$;
555
+
556
+ DO $search_paths$
557
+ DECLARE data_schema text := pg_catalog.current_schema();
558
+ BEGIN
559
+ EXECUTE format(
560
+ 'ALTER FUNCTION %I.create_organization_invitation_v2(jsonb) SET search_path = pg_catalog, %I, pg_temp',
561
+ data_schema, data_schema
562
+ );
563
+ EXECUTE format(
564
+ 'ALTER FUNCTION %I.bind_pending_organization_invitations_for_verified_email(text,text) SET search_path = pg_catalog, %I, pg_temp',
565
+ data_schema, data_schema
566
+ );
567
+ EXECUTE format(
568
+ 'ALTER FUNCTION %I.has_pending_organization_invitation_for_subject(text) SET search_path = pg_catalog, %I, pg_temp',
569
+ data_schema, data_schema
570
+ );
571
+ EXECUTE format(
572
+ 'ALTER FUNCTION %I.accept_organization_invitation_v2(jsonb) SET search_path = pg_catalog, %I, pg_temp',
573
+ data_schema, data_schema
574
+ );
575
+ END
576
+ $search_paths$;
577
+
578
+ REVOKE ALL ON FUNCTION create_organization_invitation_v2(jsonb) FROM PUBLIC;
579
+ REVOKE ALL ON FUNCTION bind_pending_organization_invitations_for_verified_email(text,text)
580
+ FROM PUBLIC;
581
+ REVOKE ALL ON FUNCTION has_pending_organization_invitation_for_subject(text) FROM PUBLIC;
582
+ REVOKE ALL ON FUNCTION accept_organization_invitation_v2(jsonb) FROM PUBLIC;
583
+ REVOKE ALL ON TABLE organization_invitation_binding_events FROM PUBLIC;
584
+
585
+ CREATE TRIGGER organization_invitation_binding_events_immutable
586
+ BEFORE UPDATE OR DELETE ON organization_invitation_binding_events
587
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.organization_membership_history_immutable();
588
+
589
+ DO $runtime_grants$
590
+ BEGIN
591
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
592
+ REVOKE ALL ON TABLE organization_invitation_binding_events FROM opengeni_app;
593
+ GRANT EXECUTE ON FUNCTION create_organization_invitation_v2(jsonb) TO opengeni_app;
594
+ GRANT EXECUTE ON FUNCTION
595
+ bind_pending_organization_invitations_for_verified_email(text,text) TO opengeni_app;
596
+ GRANT EXECUTE ON FUNCTION
597
+ has_pending_organization_invitation_for_subject(text) TO opengeni_app;
598
+ GRANT EXECUTE ON FUNCTION accept_organization_invitation_v2(jsonb) TO opengeni_app;
599
+ END IF;
600
+ END
601
+ $runtime_grants$;
602
+
603
+ COMMENT ON FUNCTION bind_pending_organization_invitations_for_verified_email(text,text) IS
604
+ 'Binds active pending email invitations only after the exact Better Auth user has verified that email.';
605
+ COMMENT ON TABLE organization_invitation_binding_events IS
606
+ 'Immutable exact-subject and revision evidence for verified-email invitation binding.';