@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,1153 @@
1
+ -- deployment-mode: rolling
2
+ -- Explicit, replay-safe Document authority reclassification and resumable
3
+ -- Default collection backfill. Existing authority is never rewritten by this
4
+ -- migration: legacy workspace rows stay workspace-scoped and legacy personal
5
+ -- rows stay anchored to their original workspace until this lifecycle is
6
+ -- called with an exact expected tuple.
7
+
8
+ SET LOCAL lock_timeout = '5s';
9
+ SET LOCAL statement_timeout = '10min';
10
+
11
+ CREATE TABLE document_authority_reclassifications (
12
+ operation_id uuid PRIMARY KEY,
13
+ input_hash text NOT NULL,
14
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE RESTRICT,
15
+ request_workspace_id uuid NOT NULL,
16
+ document_id uuid NOT NULL,
17
+ actor_subject_id text NOT NULL,
18
+ previous_authority_kind text NOT NULL,
19
+ previous_authority_workspace_id uuid,
20
+ previous_authority_subject_id text,
21
+ previous_authority_id uuid,
22
+ previous_owner_organization_membership_id uuid,
23
+ resulting_authority_kind text NOT NULL,
24
+ resulting_authority_workspace_id uuid,
25
+ resulting_authority_subject_id text,
26
+ resulting_authority_id uuid,
27
+ resulting_owner_organization_membership_id uuid,
28
+ result jsonb NOT NULL,
29
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
30
+ -- The receipt is workspace-owned evidence, so it dies with its workspace.
31
+ -- RESTRICT here would make any workspace in which a single Document was ever
32
+ -- reclassified permanently undeletable: `deleteWorkspaceIfQuiescent` relies on
33
+ -- cascades and has no quiescence branch for these tables, so the FK would
34
+ -- surface as an untyped 500 on `DELETE /v1/workspaces/:workspaceId` rather
35
+ -- than a typed status. The row-level immutability trigger below still refuses
36
+ -- every direct rewrite; only the referential cascade may remove it.
37
+ CONSTRAINT document_authority_reclassifications_workspace_fk
38
+ FOREIGN KEY (request_workspace_id, account_id)
39
+ REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
40
+ CONSTRAINT document_authority_reclassifications_hash_chk CHECK (
41
+ input_hash ~ '^[0-9a-f]{64}$'
42
+ ),
43
+ CONSTRAINT document_authority_reclassifications_actor_chk CHECK (
44
+ actor_subject_id = btrim(actor_subject_id)
45
+ AND octet_length(convert_to(actor_subject_id, 'UTF8')) BETWEEN 1 AND 1024
46
+ ),
47
+ CONSTRAINT document_authority_reclassifications_kind_chk CHECK (
48
+ previous_authority_kind IN ('organization', 'workspace', 'personal')
49
+ AND resulting_authority_kind IN ('organization', 'workspace', 'personal')
50
+ ),
51
+ CONSTRAINT document_authority_reclassifications_result_chk CHECK (
52
+ jsonb_typeof(result) = 'object'
53
+ )
54
+ );
55
+ CREATE INDEX document_authority_reclassifications_document_idx
56
+ ON document_authority_reclassifications (
57
+ account_id, request_workspace_id, document_id, created_at, operation_id
58
+ );
59
+
60
+ CREATE TABLE document_default_collection_backfill_runs (
61
+ run_id uuid PRIMARY KEY,
62
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE RESTRICT,
63
+ actor_subject_id text NOT NULL,
64
+ status text NOT NULL DEFAULT 'running',
65
+ last_workspace_id uuid,
66
+ processed_count integer NOT NULL DEFAULT 0,
67
+ created_count integer NOT NULL DEFAULT 0,
68
+ adopted_count integer NOT NULL DEFAULT 0,
69
+ started_at timestamptz NOT NULL DEFAULT clock_timestamp(),
70
+ updated_at timestamptz NOT NULL DEFAULT clock_timestamp(),
71
+ completed_at timestamptz,
72
+ CONSTRAINT document_default_collection_backfill_runs_actor_chk CHECK (
73
+ actor_subject_id = btrim(actor_subject_id)
74
+ AND octet_length(convert_to(actor_subject_id, 'UTF8')) BETWEEN 1 AND 1024
75
+ ),
76
+ CONSTRAINT document_default_collection_backfill_runs_status_chk CHECK (
77
+ (status = 'running' AND completed_at IS NULL)
78
+ OR (status = 'completed' AND completed_at IS NOT NULL)
79
+ ),
80
+ CONSTRAINT document_default_collection_backfill_runs_count_chk CHECK (
81
+ processed_count >= 0 AND created_count >= 0 AND adopted_count >= 0
82
+ AND processed_count = created_count + adopted_count
83
+ )
84
+ );
85
+
86
+ CREATE TABLE document_default_collection_backfill_operations (
87
+ operation_id uuid PRIMARY KEY,
88
+ run_id uuid NOT NULL REFERENCES document_default_collection_backfill_runs(run_id)
89
+ ON DELETE RESTRICT,
90
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE RESTRICT,
91
+ input_hash text NOT NULL,
92
+ result jsonb NOT NULL,
93
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
94
+ CONSTRAINT document_default_collection_backfill_operations_hash_chk CHECK (
95
+ input_hash ~ '^[0-9a-f]{64}$'
96
+ ),
97
+ CONSTRAINT document_default_collection_backfill_operations_result_chk CHECK (
98
+ jsonb_typeof(result) = 'object'
99
+ )
100
+ );
101
+
102
+ CREATE TABLE document_default_collection_backfill_receipts (
103
+ run_id uuid NOT NULL REFERENCES document_default_collection_backfill_runs(run_id)
104
+ ON DELETE RESTRICT,
105
+ account_id uuid NOT NULL REFERENCES managed_accounts(id) ON DELETE RESTRICT,
106
+ workspace_id uuid NOT NULL,
107
+ -- CASCADE on both parents. One admin-triggered backfill writes a receipt for
108
+ -- EVERY workspace in the account, so RESTRICT would pin the whole account as
109
+ -- undeletable after a single run - and `document_bases.workspace_id` already
110
+ -- cascades, so a RESTRICT on `base_id` would block that cascade too. The
111
+ -- receipt describes a collection inside the workspace; once the workspace,
112
+ -- its bases, and its documents are gone the receipt is evidence about
113
+ -- nothing. Direct rewrites stay refused by the immutability trigger below.
114
+ base_id uuid NOT NULL REFERENCES document_bases(id) ON DELETE CASCADE,
115
+ outcome text NOT NULL,
116
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
117
+ PRIMARY KEY (run_id, workspace_id),
118
+ CONSTRAINT document_default_collection_backfill_receipts_workspace_fk
119
+ FOREIGN KEY (workspace_id, account_id)
120
+ REFERENCES workspaces(id, account_id) ON DELETE CASCADE,
121
+ CONSTRAINT document_default_collection_backfill_receipts_outcome_chk CHECK (
122
+ outcome IN ('created', 'adopted')
123
+ )
124
+ );
125
+
126
+ CREATE TABLE opengeni_private.document_migration_capabilities (
127
+ backend_pid integer NOT NULL,
128
+ transaction_id xid8 NOT NULL,
129
+ capability_kind text NOT NULL,
130
+ created_at timestamptz NOT NULL DEFAULT clock_timestamp(),
131
+ PRIMARY KEY (backend_pid, transaction_id, capability_kind),
132
+ CONSTRAINT document_migration_capabilities_kind_chk CHECK (
133
+ capability_kind IN ('reclassify', 'default_backfill')
134
+ )
135
+ );
136
+ REVOKE ALL ON TABLE opengeni_private.document_migration_capabilities FROM PUBLIC;
137
+
138
+ CREATE OR REPLACE FUNCTION opengeni_private.document_migration_capability_active(
139
+ p_capability_kind text
140
+ ) RETURNS boolean
141
+ LANGUAGE sql
142
+ STABLE
143
+ SECURITY DEFINER
144
+ SET search_path = pg_catalog
145
+ AS $body$
146
+ SELECT EXISTS (
147
+ SELECT 1
148
+ FROM opengeni_private.document_migration_capabilities capability
149
+ WHERE capability.backend_pid = pg_catalog.pg_backend_pid()
150
+ AND capability.transaction_id = pg_catalog.pg_current_xact_id_if_assigned()
151
+ AND capability.capability_kind = p_capability_kind
152
+ )
153
+ $body$;
154
+ REVOKE ALL ON FUNCTION
155
+ opengeni_private.document_migration_capability_active(text) FROM PUBLIC;
156
+
157
+ ALTER TABLE document_authority_reclassifications ENABLE ROW LEVEL SECURITY;
158
+ ALTER TABLE document_authority_reclassifications FORCE ROW LEVEL SECURITY;
159
+ ALTER TABLE document_default_collection_backfill_runs ENABLE ROW LEVEL SECURITY;
160
+ ALTER TABLE document_default_collection_backfill_runs FORCE ROW LEVEL SECURITY;
161
+ ALTER TABLE document_default_collection_backfill_operations ENABLE ROW LEVEL SECURITY;
162
+ ALTER TABLE document_default_collection_backfill_operations FORCE ROW LEVEL SECURITY;
163
+ ALTER TABLE document_default_collection_backfill_receipts ENABLE ROW LEVEL SECURITY;
164
+ ALTER TABLE document_default_collection_backfill_receipts FORCE ROW LEVEL SECURITY;
165
+
166
+ DO $document_migration_policies$
167
+ DECLARE
168
+ data_schema text := current_schema();
169
+ migration_owner text := current_user;
170
+ table_name text;
171
+ BEGIN
172
+ EXECUTE format(
173
+ 'CREATE POLICY document_reclassification_actor_read ON %I.document_authority_reclassifications '
174
+ || 'FOR SELECT USING (account_id = nullif(current_setting(''opengeni.account_id'', true), '''')::uuid '
175
+ || 'AND request_workspace_id = nullif(current_setting(''opengeni.workspace_id'', true), '''')::uuid '
176
+ || 'AND actor_subject_id = nullif(current_setting(''opengeni.subject_id'', true), ''''))',
177
+ data_schema
178
+ );
179
+
180
+ FOREACH table_name IN ARRAY ARRAY[
181
+ 'document_authority_reclassifications',
182
+ 'document_default_collection_backfill_runs',
183
+ 'document_default_collection_backfill_operations',
184
+ 'document_default_collection_backfill_receipts'
185
+ ] LOOP
186
+ EXECUTE format(
187
+ 'CREATE POLICY document_migration_lifecycle ON %I.%I '
188
+ || 'USING (current_user = %L AND opengeni_private.document_migration_capability_active('
189
+ || 'CASE WHEN %L = ''document_authority_reclassifications'' THEN ''reclassify'' '
190
+ || 'ELSE ''default_backfill'' END)) '
191
+ || 'WITH CHECK (current_user = %L AND opengeni_private.document_migration_capability_active('
192
+ || 'CASE WHEN %L = ''document_authority_reclassifications'' THEN ''reclassify'' '
193
+ || 'ELSE ''default_backfill'' END))',
194
+ data_schema, table_name, migration_owner, table_name, migration_owner, table_name
195
+ );
196
+ END LOOP;
197
+
198
+ FOREACH table_name IN ARRAY ARRAY['documents', 'document_chunks'] LOOP
199
+ EXECUTE format(
200
+ 'CREATE POLICY document_reclassification_lifecycle ON %I.%I '
201
+ || 'USING (current_user = %L AND '
202
+ || 'opengeni_private.document_migration_capability_active(''reclassify'')) '
203
+ || 'WITH CHECK (current_user = %L AND '
204
+ || 'opengeni_private.document_migration_capability_active(''reclassify''))',
205
+ data_schema, table_name, migration_owner, migration_owner
206
+ );
207
+ END LOOP;
208
+
209
+ FOREACH table_name IN ARRAY ARRAY['workspaces', 'document_bases'] LOOP
210
+ EXECUTE format(
211
+ 'CREATE POLICY document_default_backfill_lifecycle ON %I.%I '
212
+ || 'USING (current_user = %L AND '
213
+ || 'opengeni_private.document_migration_capability_active(''default_backfill'')) '
214
+ || 'WITH CHECK (current_user = %L AND '
215
+ || 'opengeni_private.document_migration_capability_active(''default_backfill''))',
216
+ data_schema, table_name, migration_owner, migration_owner
217
+ );
218
+ END LOOP;
219
+
220
+ FOREACH table_name IN ARRAY ARRAY[
221
+ 'organization_user_resource_authorities',
222
+ 'organization_user_resource_grants'
223
+ ] LOOP
224
+ EXECUTE format(
225
+ 'CREATE POLICY document_reclassification_lifecycle ON %I.%I '
226
+ || 'USING (current_user = %L AND '
227
+ || 'opengeni_private.document_migration_capability_active(''reclassify'')) '
228
+ || 'WITH CHECK (current_user = %L AND '
229
+ || 'opengeni_private.document_migration_capability_active(''reclassify''))',
230
+ data_schema, table_name, migration_owner, migration_owner
231
+ );
232
+ END LOOP;
233
+ END
234
+ $document_migration_policies$;
235
+
236
+ CREATE FUNCTION opengeni_private.reject_document_migration_receipt_mutation()
237
+ RETURNS trigger
238
+ LANGUAGE plpgsql
239
+ AS $body$
240
+ BEGIN
241
+ -- A receipt is never rewritten and is never removed on its own. The single
242
+ -- exception is the referential cascade of an owning parent (its workspace, or
243
+ -- the Default base that workspace cascade already removes), which is what
244
+ -- keeps `DELETE /v1/workspaces/:workspaceId` working after this migration's
245
+ -- lifecycle has run. `pg_trigger_depth()` is the exact, RLS-immune
246
+ -- discriminator: a direct statement enters this trigger at depth 1, while a
247
+ -- referential action is itself executed from the parent's constraint trigger
248
+ -- and therefore enters at depth 2 or deeper. Checking whether the parent row
249
+ -- still exists would NOT work here - this trigger runs as the invoking role,
250
+ -- and `workspaces` looks empty to a FORCE-RLS-bound owner with no tenant GUC.
251
+ IF TG_OP = 'DELETE' AND pg_trigger_depth() > 1 THEN
252
+ RETURN OLD;
253
+ END IF;
254
+ RAISE EXCEPTION 'document migration receipts are immutable' USING ERRCODE = '55000';
255
+ END
256
+ $body$;
257
+ -- House pattern for an `opengeni_private` trigger function: it is invoked by the
258
+ -- trigger, never called directly, so leaving it PUBLIC-executable widens the
259
+ -- grant surface for no reason. `0333_session_turn_prompt_routing.sql` revokes
260
+ -- its equivalent the same way, and the dedicated editable-artifact materializer
261
+ -- role's grant enumeration pins exactly this.
262
+ REVOKE ALL ON FUNCTION opengeni_private.reject_document_migration_receipt_mutation() FROM PUBLIC;
263
+
264
+ CREATE TRIGGER document_authority_reclassifications_immutable
265
+ BEFORE UPDATE OR DELETE ON document_authority_reclassifications
266
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.reject_document_migration_receipt_mutation();
267
+ CREATE TRIGGER document_default_collection_backfill_operations_immutable
268
+ BEFORE UPDATE OR DELETE ON document_default_collection_backfill_operations
269
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.reject_document_migration_receipt_mutation();
270
+ CREATE TRIGGER document_default_collection_backfill_receipts_immutable
271
+ BEFORE UPDATE OR DELETE ON document_default_collection_backfill_receipts
272
+ FOR EACH ROW EXECUTE FUNCTION opengeni_private.reject_document_migration_receipt_mutation();
273
+
274
+ CREATE OR REPLACE FUNCTION opengeni_private.apply_document_authority()
275
+ RETURNS trigger
276
+ LANGUAGE plpgsql
277
+ SET search_path FROM CURRENT
278
+ AS $body$
279
+ DECLARE
280
+ reclassification_allowed boolean := false;
281
+ BEGIN
282
+ IF TG_OP = 'UPDATE' AND (
283
+ NEW.authority_kind IS DISTINCT FROM OLD.authority_kind
284
+ OR NEW.authority_workspace_id IS DISTINCT FROM OLD.authority_workspace_id
285
+ OR NEW.authority_subject_id IS DISTINCT FROM OLD.authority_subject_id
286
+ OR NEW.authority_id IS DISTINCT FROM OLD.authority_id
287
+ OR NEW.owner_organization_membership_id
288
+ IS DISTINCT FROM OLD.owner_organization_membership_id
289
+ OR NEW.origin_workspace_id IS DISTINCT FROM OLD.origin_workspace_id
290
+ OR NEW.created_by IS DISTINCT FROM OLD.created_by
291
+ OR NEW.visibility IS DISTINCT FROM OLD.visibility
292
+ ) THEN
293
+ IF opengeni_private.document_migration_capability_active('reclassify') THEN
294
+ SELECT EXISTS (
295
+ SELECT 1
296
+ FROM document_authority_reclassifications receipt
297
+ WHERE receipt.document_id = OLD.id
298
+ AND receipt.account_id = OLD.account_id
299
+ AND receipt.previous_authority_kind = OLD.authority_kind
300
+ AND receipt.previous_authority_workspace_id
301
+ IS NOT DISTINCT FROM OLD.authority_workspace_id
302
+ AND receipt.previous_authority_subject_id
303
+ IS NOT DISTINCT FROM OLD.authority_subject_id
304
+ AND receipt.previous_authority_id IS NOT DISTINCT FROM OLD.authority_id
305
+ AND receipt.previous_owner_organization_membership_id
306
+ IS NOT DISTINCT FROM OLD.owner_organization_membership_id
307
+ AND receipt.resulting_authority_kind = NEW.authority_kind
308
+ AND receipt.resulting_authority_workspace_id
309
+ IS NOT DISTINCT FROM NEW.authority_workspace_id
310
+ AND receipt.resulting_authority_subject_id
311
+ IS NOT DISTINCT FROM NEW.authority_subject_id
312
+ AND receipt.resulting_authority_id IS NOT DISTINCT FROM NEW.authority_id
313
+ AND receipt.resulting_owner_organization_membership_id
314
+ IS NOT DISTINCT FROM NEW.owner_organization_membership_id
315
+ ) INTO reclassification_allowed;
316
+ END IF;
317
+ IF NOT reclassification_allowed
318
+ OR NEW.origin_workspace_id IS DISTINCT FROM OLD.origin_workspace_id
319
+ OR NEW.created_by IS DISTINCT FROM OLD.created_by
320
+ THEN
321
+ RAISE EXCEPTION 'document authority is immutable';
322
+ END IF;
323
+ END IF;
324
+ IF NEW.authority_kind IS NULL OR (
325
+ NEW.authority_kind = 'workspace'
326
+ AND NEW.authority_workspace_id IS NULL
327
+ AND NEW.visibility = 'private'
328
+ ) THEN
329
+ NEW.authority_kind := CASE
330
+ WHEN NEW.visibility = 'private' THEN 'personal'
331
+ ELSE 'workspace'
332
+ END;
333
+ END IF;
334
+ NEW.origin_workspace_id := NEW.workspace_id;
335
+ CASE NEW.authority_kind
336
+ WHEN 'organization' THEN
337
+ NEW.authority_workspace_id := NULL;
338
+ NEW.authority_subject_id := NULL;
339
+ NEW.authority_id := NULL;
340
+ NEW.owner_organization_membership_id := NULL;
341
+ NEW.visibility := 'workspace';
342
+ WHEN 'workspace' THEN
343
+ NEW.authority_workspace_id := NEW.workspace_id;
344
+ NEW.authority_subject_id := NULL;
345
+ NEW.authority_id := NULL;
346
+ NEW.owner_organization_membership_id := NULL;
347
+ NEW.visibility := 'workspace';
348
+ WHEN 'personal' THEN
349
+ NEW.authority_subject_id := coalesce(
350
+ nullif(btrim(NEW.authority_subject_id), ''),
351
+ nullif(btrim(NEW.created_by), '')
352
+ );
353
+ IF NEW.authority_id IS NULL THEN
354
+ NEW.authority_workspace_id := NEW.workspace_id;
355
+ NEW.owner_organization_membership_id := NULL;
356
+ ELSE
357
+ NEW.authority_workspace_id := NULL;
358
+ END IF;
359
+ NEW.visibility := 'private';
360
+ ELSE
361
+ RAISE EXCEPTION 'invalid document authority kind: %', NEW.authority_kind;
362
+ END CASE;
363
+ RETURN NEW;
364
+ END
365
+ $body$;
366
+
367
+ CREATE OR REPLACE FUNCTION opengeni_private.apply_document_chunk_authority()
368
+ RETURNS trigger
369
+ LANGUAGE plpgsql
370
+ SET search_path FROM CURRENT
371
+ AS $body$
372
+ DECLARE
373
+ parent documents%ROWTYPE;
374
+ reclassification_allowed boolean := false;
375
+ BEGIN
376
+ IF TG_OP = 'UPDATE' AND (
377
+ NEW.authority_kind IS DISTINCT FROM OLD.authority_kind
378
+ OR NEW.authority_workspace_id IS DISTINCT FROM OLD.authority_workspace_id
379
+ OR NEW.authority_subject_id IS DISTINCT FROM OLD.authority_subject_id
380
+ OR NEW.authority_id IS DISTINCT FROM OLD.authority_id
381
+ OR NEW.owner_organization_membership_id
382
+ IS DISTINCT FROM OLD.owner_organization_membership_id
383
+ OR NEW.document_id IS DISTINCT FROM OLD.document_id
384
+ ) THEN
385
+ IF opengeni_private.document_migration_capability_active('reclassify') THEN
386
+ SELECT EXISTS (
387
+ SELECT 1
388
+ FROM document_authority_reclassifications receipt
389
+ WHERE receipt.document_id = OLD.document_id
390
+ AND receipt.account_id = OLD.account_id
391
+ AND receipt.previous_authority_kind = OLD.authority_kind
392
+ AND receipt.previous_authority_workspace_id
393
+ IS NOT DISTINCT FROM OLD.authority_workspace_id
394
+ AND receipt.previous_authority_subject_id
395
+ IS NOT DISTINCT FROM OLD.authority_subject_id
396
+ AND receipt.previous_authority_id IS NOT DISTINCT FROM OLD.authority_id
397
+ AND receipt.previous_owner_organization_membership_id
398
+ IS NOT DISTINCT FROM OLD.owner_organization_membership_id
399
+ AND receipt.resulting_authority_kind = NEW.authority_kind
400
+ AND receipt.resulting_authority_workspace_id
401
+ IS NOT DISTINCT FROM NEW.authority_workspace_id
402
+ AND receipt.resulting_authority_subject_id
403
+ IS NOT DISTINCT FROM NEW.authority_subject_id
404
+ AND receipt.resulting_authority_id IS NOT DISTINCT FROM NEW.authority_id
405
+ AND receipt.resulting_owner_organization_membership_id
406
+ IS NOT DISTINCT FROM NEW.owner_organization_membership_id
407
+ ) INTO reclassification_allowed;
408
+ END IF;
409
+ IF NOT reclassification_allowed OR NEW.document_id IS DISTINCT FROM OLD.document_id THEN
410
+ RAISE EXCEPTION 'document chunk authority is immutable';
411
+ END IF;
412
+ END IF;
413
+ SELECT * INTO parent FROM documents WHERE id = NEW.document_id;
414
+ IF NOT FOUND
415
+ OR parent.account_id IS DISTINCT FROM NEW.account_id
416
+ OR parent.workspace_id IS DISTINCT FROM NEW.workspace_id
417
+ OR parent.base_id IS DISTINCT FROM NEW.base_id
418
+ OR parent.file_id IS DISTINCT FROM NEW.file_id
419
+ THEN
420
+ RAISE EXCEPTION 'document chunk parent identity mismatch';
421
+ END IF;
422
+ NEW.authority_kind := parent.authority_kind;
423
+ NEW.authority_workspace_id := parent.authority_workspace_id;
424
+ NEW.authority_subject_id := parent.authority_subject_id;
425
+ NEW.authority_id := parent.authority_id;
426
+ NEW.owner_organization_membership_id := parent.owner_organization_membership_id;
427
+ RETURN NEW;
428
+ END
429
+ $body$;
430
+
431
+ CREATE FUNCTION reclassify_document_authority(p_command jsonb)
432
+ RETURNS jsonb
433
+ LANGUAGE plpgsql
434
+ SECURITY DEFINER
435
+ SET search_path FROM CURRENT
436
+ AS $body$
437
+ DECLARE
438
+ account_id_value uuid := nullif(p_command ->> 'accountId', '')::uuid;
439
+ request_workspace_id_value uuid := nullif(p_command ->> 'workspaceId', '')::uuid;
440
+ document_id_value uuid := nullif(p_command ->> 'documentId', '')::uuid;
441
+ operation_id_value uuid := nullif(p_command ->> 'operationId', '')::uuid;
442
+ actor_subject text := nullif(p_command ->> 'actorSubjectId', '');
443
+ expected_kind text := p_command #>> '{expectedAuthority,kind}';
444
+ expected_workspace_id uuid := nullif(
445
+ p_command #>> '{expectedAuthority,workspaceId}', ''
446
+ )::uuid;
447
+ expected_subject_id text := nullif(
448
+ p_command #>> '{expectedAuthority,subjectId}', ''
449
+ );
450
+ expected_authority_id uuid := nullif(
451
+ p_command #>> '{expectedAuthority,authorityId}', ''
452
+ )::uuid;
453
+ target_kind text := p_command ->> 'targetAuthorityKind';
454
+ account_admin_authorization jsonb := p_command -> 'accountAdminAuthorization';
455
+ account_admin_authorized boolean := false;
456
+ input_hash_value text;
457
+ existing_receipt document_authority_reclassifications%ROWTYPE;
458
+ document_row documents%ROWTYPE;
459
+ owner_member organization_memberships%ROWTYPE;
460
+ target_workspace_id uuid;
461
+ target_subject_id text;
462
+ target_authority_id uuid;
463
+ target_owner_membership_id uuid;
464
+ old_authority_id uuid;
465
+ receipt_created_at timestamptz;
466
+ result_value jsonb;
467
+ BEGIN
468
+ IF p_command IS NULL THEN
469
+ RAISE EXCEPTION 'document reclassification command is required' USING ERRCODE = '22023';
470
+ ELSIF account_id_value IS NULL THEN
471
+ RAISE EXCEPTION 'document reclassification account id is required' USING ERRCODE = '22023';
472
+ ELSIF request_workspace_id_value IS NULL THEN
473
+ RAISE EXCEPTION 'document reclassification workspace id is required' USING ERRCODE = '22023';
474
+ ELSIF document_id_value IS NULL THEN
475
+ RAISE EXCEPTION 'document reclassification document id is required' USING ERRCODE = '22023';
476
+ ELSIF operation_id_value IS NULL THEN
477
+ RAISE EXCEPTION 'document reclassification operation id is required' USING ERRCODE = '22023';
478
+ ELSIF actor_subject IS NULL THEN
479
+ RAISE EXCEPTION 'document reclassification actor is required' USING ERRCODE = '22023';
480
+ END IF;
481
+ IF actor_subject IS DISTINCT FROM nullif(current_setting('opengeni.subject_id', true), '') THEN
482
+ RAISE EXCEPTION 'document reclassification actor authority is invalid'
483
+ USING ERRCODE = '42501';
484
+ END IF;
485
+ IF account_id_value IS DISTINCT FROM nullif(
486
+ current_setting('opengeni.account_id', true), ''
487
+ )::uuid
488
+ THEN
489
+ RAISE EXCEPTION 'document reclassification account authority is invalid'
490
+ USING ERRCODE = '42501';
491
+ END IF;
492
+ IF request_workspace_id_value IS DISTINCT FROM nullif(
493
+ current_setting('opengeni.workspace_id', true), ''
494
+ )::uuid
495
+ THEN
496
+ RAISE EXCEPTION 'document reclassification workspace authority is invalid'
497
+ USING ERRCODE = '42501';
498
+ END IF;
499
+ -- `NULL NOT IN (...)` is NULL, not TRUE, so an ABSENT kind would slip past
500
+ -- this guard and only surface much later as the compare-and-set 40001 the API
501
+ -- renders as a 409 conflict. A missing kind is malformed input: reject it here
502
+ -- with 22023 so the caller gets a 400.
503
+ IF expected_kind IS NULL
504
+ OR target_kind IS NULL
505
+ OR expected_kind NOT IN ('organization', 'workspace', 'personal')
506
+ OR target_kind NOT IN ('organization', 'workspace', 'personal')
507
+ OR octet_length(convert_to(actor_subject, 'UTF8')) NOT BETWEEN 1 AND 1024
508
+ THEN
509
+ RAISE EXCEPTION 'document reclassification input is invalid'
510
+ USING ERRCODE = '22023';
511
+ END IF;
512
+
513
+ IF account_admin_authorization IS NOT NULL
514
+ AND account_admin_authorization <> 'null'::jsonb
515
+ THEN
516
+ account_admin_authorized :=
517
+ jsonb_typeof(account_admin_authorization) = 'object'
518
+ AND coalesce(account_admin_authorization ->> 'permission', '') = 'account:admin'
519
+ AND nullif(account_admin_authorization ->> 'accountId', '')::uuid
520
+ IS NOT DISTINCT FROM account_id_value
521
+ AND nullif(account_admin_authorization ->> 'actorSubjectId', '')
522
+ IS NOT DISTINCT FROM actor_subject
523
+ AND coalesce(account_admin_authorization ->> 'authorizationId', '')
524
+ ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
525
+ END IF;
526
+
527
+ -- The authorization stamp is fresh request provenance, not logical input.
528
+ -- Excluding it lets a later exactly-authorized HTTP retry converge on the
529
+ -- immutable operation receipt while changed business input still conflicts.
530
+ input_hash_value := encode(
531
+ sha256(convert_to((p_command - 'accountAdminAuthorization')::text, 'UTF8')),
532
+ 'hex'
533
+ );
534
+ PERFORM pg_advisory_xact_lock(
535
+ hashtextextended('document-authority-reclassification:' || operation_id_value::text, 0)
536
+ );
537
+ INSERT INTO opengeni_private.document_migration_capabilities (
538
+ backend_pid, transaction_id, capability_kind
539
+ ) VALUES (pg_backend_pid(), pg_current_xact_id(), 'reclassify')
540
+ ON CONFLICT DO NOTHING;
541
+ SELECT * INTO existing_receipt
542
+ FROM document_authority_reclassifications receipt
543
+ WHERE receipt.operation_id = operation_id_value
544
+ FOR UPDATE;
545
+ IF FOUND THEN
546
+ IF existing_receipt.account_id IS DISTINCT FROM account_id_value
547
+ OR existing_receipt.input_hash IS DISTINCT FROM input_hash_value
548
+ THEN
549
+ RAISE EXCEPTION 'document reclassification operation id was reused with different input'
550
+ USING ERRCODE = '23505';
551
+ END IF;
552
+ IF (
553
+ existing_receipt.previous_authority_kind = 'organization'
554
+ OR existing_receipt.resulting_authority_kind = 'organization'
555
+ ) AND NOT account_admin_authorized
556
+ THEN
557
+ RAISE EXCEPTION 'organization document reclassification requires exact account authority'
558
+ USING ERRCODE = '42501';
559
+ END IF;
560
+ DELETE FROM opengeni_private.document_migration_capabilities
561
+ WHERE backend_pid = pg_backend_pid()
562
+ AND transaction_id = pg_current_xact_id_if_assigned()
563
+ AND capability_kind = 'reclassify';
564
+ RETURN existing_receipt.result;
565
+ END IF;
566
+ DELETE FROM opengeni_private.document_migration_capabilities
567
+ WHERE backend_pid = pg_backend_pid()
568
+ AND transaction_id = pg_current_xact_id_if_assigned()
569
+ AND capability_kind = 'reclassify';
570
+
571
+ SELECT * INTO document_row
572
+ FROM documents document_value
573
+ WHERE document_value.id = document_id_value
574
+ AND document_value.account_id = account_id_value
575
+ FOR UPDATE;
576
+ IF NOT FOUND THEN
577
+ RAISE EXCEPTION 'document not found' USING ERRCODE = 'P0002';
578
+ END IF;
579
+ IF document_row.authority_kind IS DISTINCT FROM expected_kind
580
+ OR document_row.authority_workspace_id IS DISTINCT FROM expected_workspace_id
581
+ OR document_row.authority_subject_id IS DISTINCT FROM expected_subject_id
582
+ OR document_row.authority_id IS DISTINCT FROM expected_authority_id
583
+ THEN
584
+ RAISE EXCEPTION 'document authority changed before reclassification'
585
+ USING ERRCODE = '40001';
586
+ END IF;
587
+ IF (document_row.authority_kind = 'organization' OR target_kind = 'organization')
588
+ AND NOT account_admin_authorized
589
+ THEN
590
+ RAISE EXCEPTION 'organization document reclassification requires exact account authority'
591
+ USING ERRCODE = '42501';
592
+ END IF;
593
+ IF target_kind = 'workspace'
594
+ AND request_workspace_id_value IS DISTINCT FROM document_row.workspace_id
595
+ THEN
596
+ RAISE EXCEPTION 'workspace document target requires the immutable origin workspace route'
597
+ USING ERRCODE = '42501';
598
+ END IF;
599
+ IF target_kind = 'personal'
600
+ AND document_row.created_by IS DISTINCT FROM actor_subject
601
+ THEN
602
+ RAISE EXCEPTION 'personal document reclassification requires the original creating subject'
603
+ USING ERRCODE = '42501';
604
+ END IF;
605
+
606
+ old_authority_id := document_row.authority_id;
607
+ target_subject_id := CASE WHEN target_kind = 'personal' THEN actor_subject ELSE NULL END;
608
+ target_authority_id := CASE WHEN target_kind = 'personal' THEN document_row.authority_id ELSE NULL END;
609
+ target_owner_membership_id := CASE
610
+ WHEN target_kind = 'personal' THEN document_row.owner_organization_membership_id
611
+ ELSE NULL
612
+ END;
613
+
614
+ IF target_kind = 'personal' AND target_authority_id IS NULL THEN
615
+ -- The capability window MUST be open before the membership read, not after
616
+ -- it. `organization_memberships` is FORCE ROW LEVEL SECURITY and carries no
617
+ -- GUC-only read policy: every branch is capability- or lifecycle-gated. This
618
+ -- routine is SECURITY DEFINER, so it runs as the schema OWNER, and `FORCE`
619
+ -- binds the owner too on the documented deployment posture (a NOSUPERUSER,
620
+ -- NOBYPASSRLS migration login). Reading first would match zero rows, leave
621
+ -- `IF FOUND` false, and silently skip this entire portable-authority
622
+ -- activation - see `docs/force-rls-migration-backfills.md`.
623
+ INSERT INTO opengeni_private.personal_document_authority_capabilities (
624
+ backend_pid, transaction_id, capability_kind
625
+ ) VALUES (pg_backend_pid(), pg_current_xact_id(), 'write')
626
+ ON CONFLICT DO NOTHING;
627
+ -- Fail loudly rather than fall through to the legacy lane if the window is
628
+ -- ever not established: a blinded read here is indistinguishable from
629
+ -- "this subject holds no organization membership", and the difference is
630
+ -- whether a personal Document is portable or permanently workspace-pinned.
631
+ IF NOT opengeni_private.personal_document_authority_capability_active('write') THEN
632
+ RAISE EXCEPTION 'document reclassification personal authority window is unavailable'
633
+ USING ERRCODE = '55000';
634
+ END IF;
635
+ -- Deliberately NOT `FOR SHARE`. PostgreSQL applies a relation's UPDATE
636
+ -- policies to any SELECT that carries a locking clause, and every read
637
+ -- policy on `organization_memberships` is `FOR SELECT` only, so `FOR SHARE`
638
+ -- / `FOR KEY SHARE` / `FOR UPDATE` all return ZERO ROWS here without
639
+ -- raising - the same silent-blinding class as the FORCE-RLS backfill no-op.
640
+ -- The membership still cannot be deleted or re-keyed under us, by one of
641
+ -- two mechanisms depending on which arm the write below takes. On the
642
+ -- fresh-insert arm the RI check for
643
+ -- `organization_user_resource_authorities_membership_fk` bypasses row
644
+ -- security and takes `FOR KEY SHARE` on the exact membership row. On the
645
+ -- ON CONFLICT DO UPDATE arm no row is inserted and the FK columns are
646
+ -- unchanged, so PostgreSQL skips the RI check entirely - but the row it is
647
+ -- reactivating already references that membership under ON DELETE RESTRICT,
648
+ -- so the DELETE is refused by that reference instead. Either way a
649
+ -- concurrent non-key `UPDATE` of the membership is NOT blocked; that
650
+ -- narrower write-skew window is documented as a known residual.
651
+ SELECT membership.* INTO owner_member
652
+ FROM organization_memberships membership
653
+ WHERE membership.account_id = account_id_value
654
+ AND membership.subject_id = actor_subject
655
+ AND membership.status = 'active'
656
+ AND membership.revoked_at IS NULL;
657
+ IF FOUND THEN
658
+ -- Reactivate rather than insert blind. The rollback direction below only
659
+ -- marks the prior authority `revoked`; it never deletes the row, and
660
+ -- `organization_user_resource_authorities_resource_identity_idx` is
661
+ -- UNIQUE (account_id, organization_membership_id, resource_kind,
662
+ -- resource_id) with NO status predicate. A plain INSERT therefore made
663
+ -- the documented `personal -> workspace -> personal` round trip fail
664
+ -- permanently with 23505, which the route surfaces as an untyped 500.
665
+ -- `origin_workspace_id` is deliberately preserved on reactivation: it is
666
+ -- privacy-safe provenance of where the authority first originated, never
667
+ -- an ownership or authorization anchor, so it must not be rewritten to
668
+ -- wherever the Document happens to sit now.
669
+ INSERT INTO organization_user_resource_authorities (
670
+ id, account_id, organization_membership_id, resource_kind,
671
+ resource_id, origin_workspace_id, generation, status
672
+ ) VALUES (
673
+ gen_random_uuid(), account_id_value, owner_member.id, 'document',
674
+ document_row.id, document_row.workspace_id, 1, 'active'
675
+ )
676
+ ON CONFLICT (account_id, organization_membership_id, resource_kind, resource_id)
677
+ DO UPDATE SET
678
+ status = 'active',
679
+ generation = organization_user_resource_authorities.generation + 1,
680
+ revoked_at = NULL,
681
+ updated_at = clock_timestamp()
682
+ RETURNING id, organization_membership_id
683
+ INTO target_authority_id, target_owner_membership_id;
684
+ ELSE
685
+ -- Configured/local and rolling-legacy subjects have no organization
686
+ -- membership and keep the pre-organization workspace-anchored personal
687
+ -- lane. Close the window immediately so nothing after this point runs
688
+ -- with a capability it did not earn.
689
+ DELETE FROM opengeni_private.personal_document_authority_capabilities
690
+ WHERE backend_pid = pg_backend_pid()
691
+ AND transaction_id = pg_current_xact_id_if_assigned()
692
+ AND capability_kind = 'write';
693
+ END IF;
694
+ END IF;
695
+
696
+ -- Derived AFTER the authority decision, and never before it. The receipt and
697
+ -- the API response must state the tuple `apply_document_authority` actually
698
+ -- stores, and that trigger normalizes `authority_workspace_id` to NULL for
699
+ -- every personal row with a non-null `authority_id` (a portable personal
700
+ -- Document is anchored by its authority row, never by a workspace). Computing
701
+ -- this up front made a `personal(portable) -> personal` receipt claim an
702
+ -- anchoring that does not exist, and the trigger's receipt match compares the
703
+ -- PRE-normalization NEW row, so nothing caught it.
704
+ target_workspace_id := CASE
705
+ WHEN target_kind = 'workspace' THEN document_row.workspace_id
706
+ WHEN target_kind = 'personal' AND target_authority_id IS NULL
707
+ THEN document_row.workspace_id
708
+ ELSE NULL
709
+ END;
710
+ IF target_authority_id IS NULL THEN
711
+ target_owner_membership_id := NULL;
712
+ END IF;
713
+
714
+ -- The receipt column and the receipt's own `createdAt` must be the SAME
715
+ -- instant. Two `clock_timestamp()` calls drift by microseconds, which makes
716
+ -- the `created_at = cursor` tie-break in
717
+ -- `list_document_authority_reclassifications` unreachable and can silently
718
+ -- skip a receipt whose stored `created_at` sorts before the cursor the client
719
+ -- echoed back from `result.createdAt`.
720
+ receipt_created_at := clock_timestamp();
721
+ result_value := jsonb_build_object(
722
+ 'operationId', operation_id_value,
723
+ 'documentId', document_row.id,
724
+ 'previousAuthority', jsonb_build_object(
725
+ 'kind', document_row.authority_kind,
726
+ 'workspaceId', document_row.authority_workspace_id,
727
+ 'subjectId', document_row.authority_subject_id,
728
+ 'authorityId', document_row.authority_id
729
+ ),
730
+ 'authority', jsonb_build_object(
731
+ 'kind', target_kind,
732
+ 'workspaceId', target_workspace_id,
733
+ 'subjectId', target_subject_id,
734
+ 'authorityId', target_authority_id
735
+ ),
736
+ 'createdAt', receipt_created_at
737
+ );
738
+
739
+ INSERT INTO opengeni_private.document_migration_capabilities (
740
+ backend_pid, transaction_id, capability_kind
741
+ ) VALUES (pg_backend_pid(), pg_current_xact_id(), 'reclassify')
742
+ ON CONFLICT DO NOTHING;
743
+ INSERT INTO document_authority_reclassifications (
744
+ operation_id, input_hash, account_id, request_workspace_id, document_id,
745
+ actor_subject_id,
746
+ previous_authority_kind, previous_authority_workspace_id,
747
+ previous_authority_subject_id, previous_authority_id,
748
+ previous_owner_organization_membership_id,
749
+ resulting_authority_kind, resulting_authority_workspace_id,
750
+ resulting_authority_subject_id, resulting_authority_id,
751
+ resulting_owner_organization_membership_id, result, created_at
752
+ ) VALUES (
753
+ operation_id_value, input_hash_value, account_id_value,
754
+ request_workspace_id_value, document_row.id, actor_subject,
755
+ document_row.authority_kind, document_row.authority_workspace_id,
756
+ document_row.authority_subject_id, document_row.authority_id,
757
+ document_row.owner_organization_membership_id,
758
+ target_kind, target_workspace_id, target_subject_id, target_authority_id,
759
+ target_owner_membership_id, result_value, receipt_created_at
760
+ );
761
+
762
+ UPDATE documents SET
763
+ authority_kind = target_kind,
764
+ authority_workspace_id = target_workspace_id,
765
+ authority_subject_id = target_subject_id,
766
+ authority_id = target_authority_id,
767
+ owner_organization_membership_id = target_owner_membership_id,
768
+ visibility = CASE WHEN target_kind = 'personal' THEN 'private' ELSE 'workspace' END,
769
+ updated_at = clock_timestamp()
770
+ WHERE id = document_row.id;
771
+ UPDATE document_chunks SET
772
+ authority_kind = target_kind,
773
+ authority_workspace_id = target_workspace_id,
774
+ authority_subject_id = target_subject_id,
775
+ authority_id = target_authority_id,
776
+ owner_organization_membership_id = target_owner_membership_id
777
+ WHERE document_id = document_row.id;
778
+
779
+ IF old_authority_id IS NOT NULL AND old_authority_id IS DISTINCT FROM target_authority_id THEN
780
+ UPDATE organization_user_resource_grants SET
781
+ status = 'revoked', generation = generation + 1,
782
+ revoked_at = coalesce(revoked_at, clock_timestamp()),
783
+ updated_at = clock_timestamp()
784
+ WHERE authority_id = old_authority_id
785
+ AND account_id = account_id_value
786
+ AND status IN ('active', 'consumed');
787
+ UPDATE organization_user_resource_authorities SET
788
+ status = 'revoked', generation = generation + 1,
789
+ revoked_at = coalesce(revoked_at, clock_timestamp()),
790
+ updated_at = clock_timestamp()
791
+ WHERE id = old_authority_id
792
+ AND account_id = account_id_value
793
+ AND status <> 'revoked';
794
+ END IF;
795
+ DELETE FROM opengeni_private.personal_document_authority_capabilities
796
+ WHERE backend_pid = pg_backend_pid()
797
+ AND transaction_id = pg_current_xact_id_if_assigned()
798
+ AND capability_kind = 'write';
799
+ DELETE FROM opengeni_private.document_migration_capabilities
800
+ WHERE backend_pid = pg_backend_pid()
801
+ AND transaction_id = pg_current_xact_id_if_assigned()
802
+ AND capability_kind = 'reclassify';
803
+ RETURN result_value;
804
+ EXCEPTION WHEN OTHERS THEN
805
+ DELETE FROM opengeni_private.personal_document_authority_capabilities
806
+ WHERE backend_pid = pg_backend_pid()
807
+ AND transaction_id = pg_current_xact_id_if_assigned()
808
+ AND capability_kind = 'write';
809
+ DELETE FROM opengeni_private.document_migration_capabilities
810
+ WHERE backend_pid = pg_backend_pid()
811
+ AND transaction_id = pg_current_xact_id_if_assigned()
812
+ AND capability_kind = 'reclassify';
813
+ RAISE;
814
+ END
815
+ $body$;
816
+
817
+ CREATE FUNCTION list_document_authority_reclassifications(
818
+ p_account_id uuid,
819
+ p_workspace_id uuid,
820
+ p_actor_subject_id text,
821
+ p_document_id uuid,
822
+ p_limit integer,
823
+ p_before_created_at timestamptz,
824
+ p_before_operation_id uuid
825
+ ) RETURNS SETOF jsonb
826
+ LANGUAGE sql
827
+ STABLE
828
+ SECURITY DEFINER
829
+ SET search_path FROM CURRENT
830
+ AS $body$
831
+ SELECT receipt.result
832
+ FROM document_authority_reclassifications receipt
833
+ WHERE receipt.account_id = p_account_id
834
+ AND receipt.request_workspace_id = p_workspace_id
835
+ AND receipt.actor_subject_id = p_actor_subject_id
836
+ AND receipt.document_id = p_document_id
837
+ AND p_account_id = nullif(current_setting('opengeni.account_id', true), '')::uuid
838
+ AND p_workspace_id = nullif(current_setting('opengeni.workspace_id', true), '')::uuid
839
+ AND p_actor_subject_id = nullif(current_setting('opengeni.subject_id', true), '')
840
+ AND (
841
+ p_before_created_at IS NULL
842
+ OR receipt.created_at < p_before_created_at
843
+ OR (
844
+ receipt.created_at = p_before_created_at
845
+ AND receipt.operation_id < p_before_operation_id
846
+ )
847
+ )
848
+ ORDER BY receipt.created_at DESC, receipt.operation_id DESC
849
+ LIMIT least(greatest(coalesce(p_limit, 1), 1), 101)
850
+ $body$;
851
+
852
+ CREATE FUNCTION run_document_default_collection_backfill(p_command jsonb)
853
+ RETURNS jsonb
854
+ LANGUAGE plpgsql
855
+ SECURITY DEFINER
856
+ SET search_path FROM CURRENT
857
+ AS $body$
858
+ DECLARE
859
+ account_id_value uuid := nullif(p_command ->> 'accountId', '')::uuid;
860
+ request_workspace_id_value uuid := nullif(p_command ->> 'workspaceId', '')::uuid;
861
+ actor_subject text := nullif(p_command ->> 'actorSubjectId', '');
862
+ run_id_value uuid := nullif(p_command ->> 'runId', '')::uuid;
863
+ operation_id_value uuid := nullif(p_command ->> 'operationId', '')::uuid;
864
+ batch_size integer := coalesce((p_command ->> 'batchSize')::integer, 50);
865
+ account_admin_authorization jsonb := p_command -> 'accountAdminAuthorization';
866
+ account_admin_authorized boolean := false;
867
+ input_hash_value text;
868
+ existing_operation document_default_collection_backfill_operations%ROWTYPE;
869
+ run_row document_default_collection_backfill_runs%ROWTYPE;
870
+ workspace_row workspaces%ROWTYPE;
871
+ base_row document_bases%ROWTYPE;
872
+ outcome_value text;
873
+ batch_processed integer := 0;
874
+ batch_created integer := 0;
875
+ batch_adopted integer := 0;
876
+ has_more boolean;
877
+ result_value jsonb;
878
+ BEGIN
879
+ IF p_command IS NULL THEN
880
+ RAISE EXCEPTION 'document Default collection backfill command is required'
881
+ USING ERRCODE = '22023';
882
+ ELSIF account_id_value IS NULL THEN
883
+ RAISE EXCEPTION 'document Default collection backfill account id is required'
884
+ USING ERRCODE = '22023';
885
+ ELSIF request_workspace_id_value IS NULL THEN
886
+ RAISE EXCEPTION 'document Default collection backfill workspace id is required'
887
+ USING ERRCODE = '22023';
888
+ ELSIF actor_subject IS NULL THEN
889
+ RAISE EXCEPTION 'document Default collection backfill actor is required'
890
+ USING ERRCODE = '22023';
891
+ ELSIF run_id_value IS NULL THEN
892
+ RAISE EXCEPTION 'document Default collection backfill run id is required'
893
+ USING ERRCODE = '22023';
894
+ ELSIF operation_id_value IS NULL THEN
895
+ RAISE EXCEPTION 'document Default collection backfill operation id is required'
896
+ USING ERRCODE = '22023';
897
+ ELSIF batch_size NOT BETWEEN 1 AND 100 THEN
898
+ RAISE EXCEPTION 'document Default collection backfill batch size is invalid'
899
+ USING ERRCODE = '22023';
900
+ END IF;
901
+ IF account_id_value IS DISTINCT FROM nullif(
902
+ current_setting('opengeni.account_id', true), ''
903
+ )::uuid
904
+ THEN
905
+ RAISE EXCEPTION 'document Default collection backfill account authority is invalid'
906
+ USING ERRCODE = '42501';
907
+ END IF;
908
+ IF request_workspace_id_value IS DISTINCT FROM nullif(
909
+ current_setting('opengeni.workspace_id', true), ''
910
+ )::uuid
911
+ THEN
912
+ RAISE EXCEPTION 'document Default collection backfill workspace authority is invalid'
913
+ USING ERRCODE = '42501';
914
+ END IF;
915
+ IF actor_subject IS DISTINCT FROM nullif(
916
+ current_setting('opengeni.subject_id', true), ''
917
+ )
918
+ THEN
919
+ RAISE EXCEPTION 'document Default collection backfill actor authority is invalid'
920
+ USING ERRCODE = '42501';
921
+ END IF;
922
+ IF account_admin_authorization IS NOT NULL
923
+ AND account_admin_authorization <> 'null'::jsonb
924
+ THEN
925
+ account_admin_authorized :=
926
+ jsonb_typeof(account_admin_authorization) = 'object'
927
+ AND coalesce(account_admin_authorization ->> 'permission', '') = 'account:admin'
928
+ AND nullif(account_admin_authorization ->> 'accountId', '')::uuid
929
+ IS NOT DISTINCT FROM account_id_value
930
+ AND nullif(account_admin_authorization ->> 'actorSubjectId', '')
931
+ IS NOT DISTINCT FROM actor_subject
932
+ AND coalesce(account_admin_authorization ->> 'authorizationId', '')
933
+ ~* '^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$';
934
+ END IF;
935
+ IF NOT account_admin_authorized THEN
936
+ RAISE EXCEPTION 'document Default collection backfill requires organization administration'
937
+ USING ERRCODE = '42501';
938
+ END IF;
939
+
940
+ input_hash_value := encode(
941
+ sha256(convert_to((p_command - 'accountAdminAuthorization')::text, 'UTF8')),
942
+ 'hex'
943
+ );
944
+ PERFORM pg_advisory_xact_lock(
945
+ hashtextextended('document-default-collection-backfill:' || run_id_value::text, 0)
946
+ );
947
+ INSERT INTO opengeni_private.document_migration_capabilities (
948
+ backend_pid, transaction_id, capability_kind
949
+ ) VALUES (pg_backend_pid(), pg_current_xact_id(), 'default_backfill')
950
+ ON CONFLICT DO NOTHING;
951
+
952
+ SELECT * INTO existing_operation
953
+ FROM document_default_collection_backfill_operations operation
954
+ WHERE operation.operation_id = operation_id_value
955
+ FOR UPDATE;
956
+ IF FOUND THEN
957
+ IF existing_operation.account_id IS DISTINCT FROM account_id_value
958
+ OR existing_operation.run_id IS DISTINCT FROM run_id_value
959
+ OR existing_operation.input_hash IS DISTINCT FROM input_hash_value
960
+ THEN
961
+ RAISE EXCEPTION 'document Default backfill operation id was reused with different input'
962
+ USING ERRCODE = '23505';
963
+ END IF;
964
+ DELETE FROM opengeni_private.document_migration_capabilities
965
+ WHERE backend_pid = pg_backend_pid()
966
+ AND transaction_id = pg_current_xact_id_if_assigned()
967
+ AND capability_kind = 'default_backfill';
968
+ RETURN existing_operation.result;
969
+ END IF;
970
+
971
+ SELECT * INTO run_row
972
+ FROM document_default_collection_backfill_runs run
973
+ WHERE run.run_id = run_id_value
974
+ FOR UPDATE;
975
+ IF NOT FOUND THEN
976
+ INSERT INTO document_default_collection_backfill_runs (
977
+ run_id, account_id, actor_subject_id
978
+ ) VALUES (run_id_value, account_id_value, actor_subject)
979
+ RETURNING * INTO run_row;
980
+ ELSIF run_row.account_id IS DISTINCT FROM account_id_value
981
+ OR run_row.actor_subject_id IS DISTINCT FROM actor_subject
982
+ THEN
983
+ RAISE EXCEPTION 'document Default backfill run identity mismatch'
984
+ USING ERRCODE = '23505';
985
+ END IF;
986
+
987
+ IF run_row.status = 'running' THEN
988
+ FOR workspace_row IN
989
+ SELECT workspace.* FROM workspaces workspace
990
+ WHERE workspace.account_id = account_id_value
991
+ AND (run_row.last_workspace_id IS NULL OR workspace.id > run_row.last_workspace_id)
992
+ ORDER BY workspace.id
993
+ LIMIT batch_size
994
+ FOR UPDATE
995
+ LOOP
996
+ SELECT * INTO base_row
997
+ FROM document_bases base
998
+ WHERE base.workspace_id = workspace_row.id
999
+ AND lower(btrim(base.name)) = 'default'
1000
+ ORDER BY base.id
1001
+ LIMIT 1
1002
+ FOR UPDATE;
1003
+ IF FOUND THEN
1004
+ outcome_value := 'adopted';
1005
+ batch_adopted := batch_adopted + 1;
1006
+ ELSE
1007
+ INSERT INTO document_bases (
1008
+ account_id, workspace_id, name, description
1009
+ ) VALUES (
1010
+ account_id_value, workspace_row.id, 'Default',
1011
+ 'Default base for dropped files and notes.'
1012
+ )
1013
+ ON CONFLICT DO NOTHING
1014
+ RETURNING * INTO base_row;
1015
+ IF FOUND THEN
1016
+ outcome_value := 'created';
1017
+ batch_created := batch_created + 1;
1018
+ ELSE
1019
+ SELECT * INTO STRICT base_row FROM document_bases base
1020
+ WHERE base.workspace_id = workspace_row.id
1021
+ AND lower(btrim(base.name)) = 'default'
1022
+ ORDER BY base.id LIMIT 1;
1023
+ outcome_value := 'adopted';
1024
+ batch_adopted := batch_adopted + 1;
1025
+ END IF;
1026
+ END IF;
1027
+ INSERT INTO document_default_collection_backfill_receipts (
1028
+ run_id, account_id, workspace_id, base_id, outcome
1029
+ ) VALUES (
1030
+ run_id_value, account_id_value, workspace_row.id, base_row.id, outcome_value
1031
+ ) ON CONFLICT (run_id, workspace_id) DO NOTHING;
1032
+ run_row.last_workspace_id := workspace_row.id;
1033
+ batch_processed := batch_processed + 1;
1034
+ END LOOP;
1035
+
1036
+ SELECT EXISTS (
1037
+ SELECT 1 FROM workspaces workspace
1038
+ WHERE workspace.account_id = account_id_value
1039
+ AND (run_row.last_workspace_id IS NULL OR workspace.id > run_row.last_workspace_id)
1040
+ ) INTO has_more;
1041
+ UPDATE document_default_collection_backfill_runs SET
1042
+ last_workspace_id = run_row.last_workspace_id,
1043
+ processed_count = processed_count + batch_processed,
1044
+ created_count = created_count + batch_created,
1045
+ adopted_count = adopted_count + batch_adopted,
1046
+ status = CASE WHEN has_more THEN 'running' ELSE 'completed' END,
1047
+ updated_at = clock_timestamp(),
1048
+ completed_at = CASE WHEN has_more THEN NULL ELSE clock_timestamp() END
1049
+ WHERE run_id = run_id_value
1050
+ RETURNING * INTO run_row;
1051
+ END IF;
1052
+
1053
+ result_value := jsonb_build_object(
1054
+ 'runId', run_row.run_id,
1055
+ 'operationId', operation_id_value,
1056
+ 'status', run_row.status,
1057
+ 'lastWorkspaceId', run_row.last_workspace_id,
1058
+ 'processedCount', run_row.processed_count,
1059
+ 'createdCount', run_row.created_count,
1060
+ 'adoptedCount', run_row.adopted_count,
1061
+ 'completedAt', run_row.completed_at
1062
+ );
1063
+ INSERT INTO document_default_collection_backfill_operations (
1064
+ operation_id, run_id, account_id, input_hash, result
1065
+ ) VALUES (
1066
+ operation_id_value, run_id_value, account_id_value, input_hash_value, result_value
1067
+ );
1068
+ DELETE FROM opengeni_private.document_migration_capabilities
1069
+ WHERE backend_pid = pg_backend_pid()
1070
+ AND transaction_id = pg_current_xact_id_if_assigned()
1071
+ AND capability_kind = 'default_backfill';
1072
+ RETURN result_value;
1073
+ EXCEPTION WHEN OTHERS THEN
1074
+ DELETE FROM opengeni_private.document_migration_capabilities
1075
+ WHERE backend_pid = pg_backend_pid()
1076
+ AND transaction_id = pg_current_xact_id_if_assigned()
1077
+ AND capability_kind = 'default_backfill';
1078
+ RAISE;
1079
+ END
1080
+ $body$;
1081
+
1082
+ DO $document_migration_search_path$
1083
+ DECLARE
1084
+ data_schema text := current_schema();
1085
+ BEGIN
1086
+ EXECUTE format(
1087
+ 'ALTER FUNCTION opengeni_private.apply_document_authority() '
1088
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1089
+ data_schema
1090
+ );
1091
+ EXECUTE format(
1092
+ 'ALTER FUNCTION opengeni_private.apply_document_chunk_authority() '
1093
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1094
+ data_schema
1095
+ );
1096
+ EXECUTE format(
1097
+ 'ALTER FUNCTION %I.reclassify_document_authority(jsonb) '
1098
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1099
+ data_schema, data_schema
1100
+ );
1101
+ EXECUTE format(
1102
+ 'ALTER FUNCTION %I.list_document_authority_reclassifications('
1103
+ || 'uuid,uuid,text,uuid,integer,timestamptz,uuid) '
1104
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1105
+ data_schema, data_schema
1106
+ );
1107
+ EXECUTE format(
1108
+ 'ALTER FUNCTION %I.run_document_default_collection_backfill(jsonb) '
1109
+ || 'SET search_path = pg_catalog, %I, pg_temp',
1110
+ data_schema, data_schema
1111
+ );
1112
+ END
1113
+ $document_migration_search_path$;
1114
+
1115
+ REVOKE ALL ON TABLE document_authority_reclassifications FROM PUBLIC;
1116
+ REVOKE ALL ON TABLE document_default_collection_backfill_runs FROM PUBLIC;
1117
+ REVOKE ALL ON TABLE document_default_collection_backfill_operations FROM PUBLIC;
1118
+ REVOKE ALL ON TABLE document_default_collection_backfill_receipts FROM PUBLIC;
1119
+ REVOKE ALL ON FUNCTION reclassify_document_authority(jsonb) FROM PUBLIC;
1120
+ REVOKE ALL ON FUNCTION
1121
+ list_document_authority_reclassifications(
1122
+ uuid, uuid, text, uuid, integer, timestamptz, uuid
1123
+ ) FROM PUBLIC;
1124
+ REVOKE ALL ON FUNCTION run_document_default_collection_backfill(jsonb) FROM PUBLIC;
1125
+
1126
+ DO $document_migration_runtime_grants$
1127
+ BEGIN
1128
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
1129
+ GRANT SELECT ON TABLE document_authority_reclassifications TO opengeni_app;
1130
+ REVOKE INSERT, UPDATE, DELETE ON TABLE document_authority_reclassifications
1131
+ FROM opengeni_app;
1132
+ REVOKE ALL ON TABLE document_default_collection_backfill_runs FROM opengeni_app;
1133
+ REVOKE ALL ON TABLE document_default_collection_backfill_operations FROM opengeni_app;
1134
+ REVOKE ALL ON TABLE document_default_collection_backfill_receipts FROM opengeni_app;
1135
+ GRANT EXECUTE ON FUNCTION reclassify_document_authority(jsonb) TO opengeni_app;
1136
+ GRANT EXECUTE ON FUNCTION
1137
+ list_document_authority_reclassifications(
1138
+ uuid, uuid, text, uuid, integer, timestamptz, uuid
1139
+ ) TO opengeni_app;
1140
+ GRANT EXECUTE ON FUNCTION run_document_default_collection_backfill(jsonb) TO opengeni_app;
1141
+ GRANT EXECUTE ON FUNCTION
1142
+ opengeni_private.document_migration_capability_active(text) TO opengeni_app;
1143
+ REVOKE ALL ON TABLE opengeni_private.document_migration_capabilities FROM opengeni_app;
1144
+ END IF;
1145
+ END
1146
+ $document_migration_runtime_grants$;
1147
+
1148
+ COMMENT ON TABLE document_authority_reclassifications IS
1149
+ 'Immutable same-transaction before/after receipts for explicit Document authority changes.';
1150
+ COMMENT ON TABLE document_default_collection_backfill_runs IS
1151
+ 'Resumable account-bounded creation/adoption progress for the internal Default collection.';
1152
+ COMMENT ON TABLE document_default_collection_backfill_receipts IS
1153
+ 'Immutable per-workspace evidence for each Default collection backfill run.';