@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,377 @@
1
+ import {
2
+ PersonalResourceAttachmentSummary,
3
+ type PersonalResourceAttachmentIntent,
4
+ USER_RESOURCE_ACTION_BY_KIND,
5
+ type UserResourceDelegation,
6
+ type UserResourceKind,
7
+ } from "@opengeni/contracts";
8
+ import { sql } from "drizzle-orm";
9
+ import { rawRows, setSubjectRlsContext, withRlsContext, type Database } from "./database";
10
+ import { nestedPostgresSqlState } from "./persistence-errors";
11
+
12
+ export type UserResourceAuthorityGrant = {
13
+ grantId: string;
14
+ targetWorkspaceId: string;
15
+ targetSessionId: string | null;
16
+ action: (typeof USER_RESOURCE_ACTION_BY_KIND)[UserResourceKind];
17
+ mode: "once" | "session" | "always";
18
+ context: "user_private" | "workspace_shared";
19
+ authorityEpoch: number | null;
20
+ generation: number;
21
+ status: "active" | "consumed" | "revoked" | "expired";
22
+ expiresAt: string | null;
23
+ delegation: UserResourceDelegation;
24
+ };
25
+
26
+ export type UserResourceAuthoritySummary = {
27
+ authorityId: string;
28
+ resourceKind: UserResourceKind;
29
+ resourceId: string;
30
+ originWorkspaceId: string | null;
31
+ generation: number;
32
+ status: "active" | "retained" | "revoked";
33
+ grants: UserResourceAuthorityGrant[];
34
+ };
35
+
36
+ export type AcceptedTurnPersonalResourceAttachment = {
37
+ summary: PersonalResourceAttachmentSummary;
38
+ replay: boolean;
39
+ };
40
+
41
+ export class PersonalResourceAttachmentAcceptanceError extends Error {
42
+ readonly name = "PersonalResourceAttachmentAcceptanceError";
43
+ constructor(
44
+ readonly kind: "invalid" | "forbidden" | "conflict",
45
+ options?: ErrorOptions,
46
+ ) {
47
+ super(
48
+ kind === "invalid"
49
+ ? "The personal-resource attachment request is invalid"
50
+ : kind === "forbidden"
51
+ ? "The personal-resource attachment is not authorized"
52
+ : "The personal-resource attachment conflicts with accepted work",
53
+ options,
54
+ );
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Issue and freeze the locked session's personal Variable Set/Rig/Connected
60
+ * Machine closure on
61
+ * one already-inserted logical turn. The caller owns the surrounding accepted-
62
+ * work transaction; this helper must never be called as a standalone grant
63
+ * mutation.
64
+ */
65
+ export async function acceptTurnPersonalResourceAttachmentInTransaction(
66
+ db: Database,
67
+ input: {
68
+ accountId: string;
69
+ workspaceId: string;
70
+ sessionId: string;
71
+ turnId: string;
72
+ subjectId: string;
73
+ intent: PersonalResourceAttachmentIntent & { expectedAuthorityEpoch: number };
74
+ },
75
+ ): Promise<AcceptedTurnPersonalResourceAttachment> {
76
+ await setSubjectRlsContext(db, input.subjectId);
77
+ let row:
78
+ | {
79
+ grantMode: "once" | "session" | "always";
80
+ grantContext: "user_private" | "workspace_shared";
81
+ resourceCount: number;
82
+ resourceKinds: Array<"variable_set" | "rig" | "connected_machine">;
83
+ sharedOutputWarningVersion: 1;
84
+ replay: boolean;
85
+ }
86
+ | undefined;
87
+ try {
88
+ [row] = await rawRows(
89
+ db,
90
+ sql`
91
+ select grant_mode as "grantMode", grant_context as "grantContext",
92
+ resource_count::int as "resourceCount", resource_kinds as "resourceKinds",
93
+ shared_output_warning_version::int as "sharedOutputWarningVersion", replay
94
+ from accept_turn_personal_resource_attachment(
95
+ ${input.accountId}::uuid, ${input.workspaceId}::uuid,
96
+ ${input.sessionId}::uuid, ${input.turnId}::uuid, ${input.intent.mode},
97
+ ${input.intent.expectedAuthorityEpoch},
98
+ ${input.intent.workspaceSharedAcknowledged},
99
+ ${input.intent.sharedOutputWarningVersion}
100
+ )
101
+ `,
102
+ );
103
+ } catch (error) {
104
+ const sqlState = nestedPostgresSqlState(error);
105
+ if (sqlState === "22023") {
106
+ throw new PersonalResourceAttachmentAcceptanceError("invalid", { cause: error });
107
+ }
108
+ if (sqlState === "42501") {
109
+ throw new PersonalResourceAttachmentAcceptanceError("forbidden", { cause: error });
110
+ }
111
+ if (sqlState === "23505") {
112
+ throw new PersonalResourceAttachmentAcceptanceError("conflict", { cause: error });
113
+ }
114
+ throw error;
115
+ }
116
+ if (!row) throw new Error("atomic personal-resource attachment was not returned");
117
+ return {
118
+ summary: PersonalResourceAttachmentSummary.parse({
119
+ mode: row.grantMode,
120
+ context: row.grantContext,
121
+ resourceCount: row.resourceCount,
122
+ resourceKinds: row.resourceKinds,
123
+ sharedOutputWarningVersion: row.sharedOutputWarningVersion,
124
+ }),
125
+ replay: row.replay,
126
+ };
127
+ }
128
+
129
+ type AuthorityRow = Omit<
130
+ UserResourceAuthorityGrant,
131
+ "grantId" | "targetWorkspaceId" | "delegation"
132
+ > & {
133
+ authorityId: string;
134
+ organizationId: string;
135
+ resourceKind: UserResourceKind;
136
+ resourceId: string;
137
+ originWorkspaceId: string | null;
138
+ authorityGeneration: number;
139
+ authorityStatus: UserResourceAuthoritySummary["status"];
140
+ grantId: string | null;
141
+ targetWorkspaceId: string | null;
142
+ };
143
+
144
+ function rfc3339(value: string | null): string | null {
145
+ if (value === null) return null;
146
+ const date = new Date(value);
147
+ if (Number.isNaN(date.getTime())) throw new Error("invalid database timestamptz projection");
148
+ return date.toISOString();
149
+ }
150
+
151
+ async function withOwnerContext<T>(
152
+ db: Database,
153
+ input: { accountId: string; workspaceId: string; subjectId: string },
154
+ fn: (db: Database) => Promise<T>,
155
+ ): Promise<T> {
156
+ return await withRlsContext(db, input, async (scopedDb) => {
157
+ await setSubjectRlsContext(scopedDb, input.subjectId);
158
+ return await fn(scopedDb);
159
+ });
160
+ }
161
+
162
+ export async function listSelfUserResourceAuthorities(
163
+ db: Database,
164
+ input: {
165
+ accountId: string;
166
+ workspaceId: string;
167
+ subjectId: string;
168
+ resourceKind: UserResourceKind;
169
+ cursor?: string | undefined;
170
+ limit: number;
171
+ },
172
+ ): Promise<{ authorities: UserResourceAuthoritySummary[]; nextCursor: string | null }> {
173
+ return await withOwnerContext(db, input, async (scopedDb) => {
174
+ const rows = await rawRows<AuthorityRow>(
175
+ scopedDb,
176
+ sql`
177
+ select authority_id as "authorityId", organization_id as "organizationId",
178
+ resource_kind as "resourceKind", resource_id as "resourceId",
179
+ origin_workspace_id as "originWorkspaceId",
180
+ authority_generation::int as "authorityGeneration",
181
+ authority_status as "authorityStatus", grant_id as "grantId",
182
+ target_workspace_id as "targetWorkspaceId", target_session_id as "targetSessionId",
183
+ action, grant_mode as mode, grant_context as context,
184
+ authority_epoch::int as "authorityEpoch",
185
+ grant_generation::int as generation, grant_status as status,
186
+ expires_at::text as "expiresAt"
187
+ from list_self_user_resource_authorities(
188
+ ${input.accountId}::uuid, ${input.workspaceId}::uuid, ${input.resourceKind},
189
+ ${input.cursor ?? null}::uuid, ${input.limit + 1}
190
+ )
191
+ `,
192
+ );
193
+ const grouped = new Map<string, UserResourceAuthoritySummary>();
194
+ for (const row of rows) {
195
+ const authority = grouped.get(row.authorityId) ?? {
196
+ authorityId: row.authorityId,
197
+ resourceKind: row.resourceKind,
198
+ resourceId: row.resourceId,
199
+ originWorkspaceId: row.originWorkspaceId,
200
+ generation: row.authorityGeneration,
201
+ status: row.authorityStatus,
202
+ grants: [],
203
+ };
204
+ if (row.grantId && row.targetWorkspaceId) {
205
+ authority.grants.push({
206
+ grantId: row.grantId,
207
+ targetWorkspaceId: row.targetWorkspaceId,
208
+ targetSessionId: row.targetSessionId,
209
+ action: row.action,
210
+ mode: row.mode,
211
+ context: row.context,
212
+ authorityEpoch: row.authorityEpoch,
213
+ generation: row.generation,
214
+ status: row.status,
215
+ expiresAt: rfc3339(row.expiresAt),
216
+ delegation: {
217
+ authorityId: row.authorityId,
218
+ grantId: row.grantId,
219
+ organizationId: row.organizationId,
220
+ workspaceId: row.targetWorkspaceId,
221
+ sessionId: row.targetSessionId,
222
+ action: row.action,
223
+ mode: row.mode,
224
+ context: row.context,
225
+ authorityEpoch: row.authorityEpoch,
226
+ authorityGeneration: row.authorityGeneration,
227
+ grantGeneration: row.generation,
228
+ },
229
+ });
230
+ }
231
+ grouped.set(row.authorityId, authority);
232
+ }
233
+ const values = [...grouped.values()];
234
+ const hasMore = values.length > input.limit;
235
+ const authorities = hasMore ? values.slice(0, input.limit) : values;
236
+ return {
237
+ authorities,
238
+ nextCursor: hasMore ? (authorities.at(-1)?.authorityId ?? null) : null,
239
+ };
240
+ });
241
+ }
242
+
243
+ export async function issueSelfUserResourceGrant(
244
+ db: Database,
245
+ input: {
246
+ accountId: string;
247
+ workspaceId: string;
248
+ subjectId: string;
249
+ authorityId: string;
250
+ resourceKind: UserResourceKind;
251
+ mode: "session" | "always";
252
+ context: "user_private" | "workspace_shared";
253
+ sessionId?: string | null;
254
+ expectedAuthorityEpoch?: number | null;
255
+ workspaceSharedAcknowledged: boolean;
256
+ },
257
+ ): Promise<UserResourceAuthorityGrant> {
258
+ return await withOwnerContext(db, input, async (scopedDb) => {
259
+ const [row] = await rawRows<
260
+ Omit<UserResourceAuthorityGrant, "delegation"> & {
261
+ organizationId: string;
262
+ authorityGeneration: number;
263
+ }
264
+ >(
265
+ scopedDb,
266
+ sql`
267
+ select grant_id as "grantId", organization_id as "organizationId",
268
+ authority_generation::int as "authorityGeneration",
269
+ target_workspace_id as "targetWorkspaceId",
270
+ target_session_id as "targetSessionId", action, grant_mode as mode,
271
+ grant_context as context, authority_epoch::int as "authorityEpoch",
272
+ grant_generation::int as generation,
273
+ grant_status as status, expires_at::text as "expiresAt"
274
+ from issue_self_user_resource_grant(
275
+ ${input.accountId}::uuid, ${input.authorityId}::uuid, ${input.workspaceId}::uuid,
276
+ ${input.resourceKind}, ${input.mode}, ${input.context},
277
+ ${input.sessionId ?? null}::uuid, ${input.expectedAuthorityEpoch ?? null}::integer,
278
+ ${input.workspaceSharedAcknowledged}
279
+ )
280
+ `,
281
+ );
282
+ if (!row) throw new Error("user-resource grant was not returned");
283
+ const { organizationId, authorityGeneration, ...grant } = row;
284
+ return {
285
+ ...grant,
286
+ expiresAt: rfc3339(grant.expiresAt),
287
+ delegation: {
288
+ authorityId: input.authorityId,
289
+ grantId: grant.grantId,
290
+ organizationId,
291
+ workspaceId: grant.targetWorkspaceId,
292
+ sessionId: grant.targetSessionId,
293
+ action: grant.action,
294
+ mode: grant.mode,
295
+ context: grant.context,
296
+ authorityEpoch: grant.authorityEpoch,
297
+ authorityGeneration,
298
+ grantGeneration: grant.generation,
299
+ },
300
+ };
301
+ });
302
+ }
303
+
304
+ export async function revokeSelfUserResourceGrant(
305
+ db: Database,
306
+ input: { accountId: string; workspaceId: string; subjectId: string; grantId: string },
307
+ ): Promise<{ grantId: string; generation: number; status: "revoked"; revokedAt: string }> {
308
+ return await withOwnerContext(db, input, async (scopedDb) => {
309
+ const [row] = await rawRows<{
310
+ grantId: string;
311
+ generation: number;
312
+ status: "revoked";
313
+ revokedAt: string;
314
+ }>(
315
+ scopedDb,
316
+ sql`
317
+ select grant_id as "grantId", grant_generation::int as generation,
318
+ grant_status as status, revoked_at::text as "revokedAt"
319
+ from revoke_self_user_resource_grant(
320
+ ${input.accountId}::uuid, ${input.workspaceId}::uuid, ${input.grantId}::uuid
321
+ )
322
+ `,
323
+ );
324
+ if (!row) throw new Error("user-resource grant was not returned");
325
+ return { ...row, revokedAt: rfc3339(row.revokedAt)! };
326
+ });
327
+ }
328
+
329
+ export async function resolveSessionAttemptPersonalResources(
330
+ db: Database,
331
+ input: { accountId: string; workspaceId: string; subjectId: string | null; attemptId: string },
332
+ ): Promise<void> {
333
+ await withRlsContext(db, input, async (scopedDb) => {
334
+ if (input.subjectId) await setSubjectRlsContext(scopedDb, input.subjectId);
335
+ await scopedDb.execute(sql`select authorize_session_attempt_personal_resource_reads(
336
+ ${input.accountId}::uuid, ${input.workspaceId}::uuid, ${input.attemptId}::uuid
337
+ )`);
338
+ });
339
+ }
340
+
341
+ export type PersonalDocumentAuthority = {
342
+ authorityId: string;
343
+ ownerOrganizationMembershipId: string;
344
+ authorityGeneration: number;
345
+ };
346
+
347
+ /**
348
+ * Create the common organization-user authority before inserting a personal
349
+ * Document. Returns null for a configured/local or rolling-legacy subject that
350
+ * has no eligible active organization membership, preserving the existing
351
+ * workspace-anchored personal lane. Call this inside the same database
352
+ * transaction as the Document insert so a failed write cannot leave an orphan
353
+ * authority.
354
+ */
355
+ export async function createPersonalDocumentAuthority(
356
+ db: Database,
357
+ input: {
358
+ accountId: string;
359
+ workspaceId: string;
360
+ subjectId: string;
361
+ documentId: string;
362
+ },
363
+ ): Promise<PersonalDocumentAuthority | null> {
364
+ await setSubjectRlsContext(db, input.subjectId);
365
+ const [row] = await rawRows<PersonalDocumentAuthority>(
366
+ db,
367
+ sql`
368
+ select authority_id as "authorityId",
369
+ owner_organization_membership_id as "ownerOrganizationMembershipId",
370
+ authority_generation::int as "authorityGeneration"
371
+ from create_personal_document_authority(
372
+ ${input.accountId}::uuid, ${input.workspaceId}::uuid, ${input.documentId}::uuid
373
+ )
374
+ `,
375
+ );
376
+ return row ?? null;
377
+ }
@@ -0,0 +1,160 @@
1
+ import { OrganizationMember } from "@opengeni/contracts";
2
+ import { sql } from "drizzle-orm";
3
+ import { rawRows, type Database } from "./database";
4
+
5
+ /**
6
+ * THE canonical live-workspace-authority resolver for one exact subject, run on
7
+ * a handle that is ALREADY inside the caller's transaction with that subject's
8
+ * RLS context applied.
9
+ *
10
+ * `getWorkspaceGrant` is a bare `workspace_memberships` join and is therefore
11
+ * NOT a complete authority answer: a managed human's personal workspace
12
+ * deliberately has no row in that table (migration 0219 raises on one), so its
13
+ * owner's access is the `organization_memberships.personal_workspace_id`
14
+ * pointer instead. Any seam that re-derives "does this subject still hold
15
+ * workspace authority here" from that bare join denies the one human who always
16
+ * belongs. This module exists so that rule has exactly one implementation.
17
+ *
18
+ * Seams that must serialize the answer with their own writes — session listing,
19
+ * session pins, the composer draft — call this variant so the read and the write
20
+ * share one transaction snapshot and one advisory fence. Seams that need to ask
21
+ * about a subject that is NOT the caller call
22
+ * {@link namedSubjectHasLiveWorkspaceAuthority} (`@opengeni/db`), which is a thin
23
+ * scope wrapper over exactly this body.
24
+ *
25
+ * True when the subject currently holds workspace authority the same way the
26
+ * route-time access builder derives it: a `workspace_memberships` row whose
27
+ * owning organization membership (when one exists) is active, or an active
28
+ * organization membership whose personal-workspace pointer is exactly this
29
+ * workspace. Never infers authority any other way; a deleted membership row and
30
+ * a suspended/revoked organization membership are both false. Only the
31
+ * membership's own `personal_workspace_id` counts as stated authority — nothing
32
+ * is read from `created_by`, a default workspace, a resource name, or current
33
+ * access.
34
+ *
35
+ * # This function is NOT an authorization. Read this before relying on it.
36
+ *
37
+ * It answers "does subject X hold authority over workspace W". It does NOT
38
+ * establish that the caller is X. Nothing here authenticates anybody.
39
+ *
40
+ * The scope assertion below is a **consistency check, not an authorization**.
41
+ * It prevents one thing: a caller naming a third-party subject while its
42
+ * transaction is scoped to someone else. It cannot detect a caller that scoped
43
+ * the transaction to an attacker-chosen subject in the first place, because
44
+ * `withWorkspaceSubjectRls(db, workspaceId, subjectId, ...)` takes that subject
45
+ * from its argument too. At today's three call sites both sides come from the
46
+ * same variable, so the check is a tautology there and fires only if a future
47
+ * refactor makes them diverge. Treat it as a tripwire against that refactor —
48
+ * never as evidence that the subject was authenticated.
49
+ *
50
+ * **The actual authorization for the personal-workspace exception lives in the
51
+ * API layer**: `AccessGrantAuthorization.canonicalManagedHumanSession`
52
+ * (`@opengeni/core`), stamped only where a Better Auth cookie was verified. A
53
+ * caller that has not proven that must not enable the exception. If you are
54
+ * adding a caller, that stamp is the part you have to get right; this function
55
+ * will happily answer `true` for any subject you scope a transaction to.
56
+ *
57
+ * Non-`user:` subjects (API keys, delegated service principals, configured and
58
+ * local principals) can never own an organization membership, so they
59
+ * short-circuit to the plain membership answer instead of tripping
60
+ * `list_self_organization_memberships`' `42501`.
61
+ */
62
+ export async function subjectHasLiveWorkspaceAuthorityInScope(
63
+ scopedDb: Database,
64
+ input: { accountId: string; workspaceId: string; subjectId: string },
65
+ ): Promise<boolean> {
66
+ // CONSISTENCY CHECK, NOT AN AUTHORIZATION. This function deliberately does not
67
+ // set `opengeni.subject_id`, so it cannot be handed a scope of its own — that
68
+ // makes the oracle shape unrepresentable here. But the scope it inherits was
69
+ // set by `withWorkspaceSubjectRls` from ITS caller's argument, so a match
70
+ // proves only that one caller used one subject consistently; it proves
71
+ // nothing about who that subject is. Authorization is the API layer's
72
+ // canonical-managed-cookie stamp. See the doc comment above.
73
+ const scopedSubjectId = await subjectIdInRlsScope(scopedDb);
74
+ if (scopedSubjectId !== input.subjectId) {
75
+ throw new Error(
76
+ "subjectHasLiveWorkspaceAuthorityInScope: subject does not match the applied RLS scope",
77
+ );
78
+ }
79
+ const [membershipRow] = await rawRows<{ present: number }>(
80
+ scopedDb,
81
+ sql`select 1 as present from workspace_memberships
82
+ where subject_id = ${input.subjectId}
83
+ and workspace_id = ${input.workspaceId}
84
+ limit 1`,
85
+ );
86
+ if (!input.subjectId.startsWith("user:")) {
87
+ return Boolean(membershipRow);
88
+ }
89
+ const [organizationMembershipResult] = await rawRows<{ result: unknown }>(
90
+ scopedDb,
91
+ sql`select list_self_organization_memberships(${input.subjectId}) as result`,
92
+ );
93
+ const organizationMemberships = OrganizationMember.array().parse(
94
+ organizationMembershipResult?.result ?? [],
95
+ );
96
+ const selfOrganizationMembership = organizationMemberships.find(
97
+ (organizationMembership) => organizationMembership.organizationId === input.accountId,
98
+ );
99
+ if (membershipRow) {
100
+ // Mirror the access builder's inactive-organization filter: a persisted
101
+ // membership row is dead while its organization membership is
102
+ // suspended/revoked. A subject with no organization membership at all
103
+ // (legacy/standalone) keeps its persisted row's authority.
104
+ return !selfOrganizationMembership || selfOrganizationMembership.status === "active";
105
+ }
106
+ return (
107
+ selfOrganizationMembership?.status === "active" &&
108
+ selfOrganizationMembership.personalWorkspaceId === input.workspaceId
109
+ );
110
+ }
111
+
112
+ /**
113
+ * The applied subject GUC, or `""` when none is set. `""` is the canonical
114
+ * "unset" for this GUC: `current_subject_id()` is
115
+ * `nullif(current_setting('opengeni.subject_id', true), '')` (0239 precedent), so
116
+ * a caller restoring `""` genuinely clears it rather than pinning an empty
117
+ * subject. Never throws, so it is safe for capturing a prior value that may
118
+ * legitimately be absent.
119
+ */
120
+ export async function rlsSubjectIdOrEmpty(scopedDb: Database): Promise<string> {
121
+ const [row] = await rawRows<{ subjectId: string | null }>(
122
+ scopedDb,
123
+ sql`select current_setting('opengeni.subject_id', true) as "subjectId"`,
124
+ );
125
+ return row?.subjectId ?? "";
126
+ }
127
+
128
+ /**
129
+ * The subject of the caller's ALREADY-applied RLS scope, required to be present.
130
+ * Used by the in-scope resolver to check that a caller is asking about its own
131
+ * scope rather than naming a third party — a consistency check, not proof that
132
+ * anything authenticated that subject.
133
+ */
134
+ async function subjectIdInRlsScope(scopedDb: Database): Promise<string> {
135
+ const subjectId = await rlsSubjectIdOrEmpty(scopedDb);
136
+ if (!subjectId) {
137
+ throw new Error("RLS subject context is not applied on the active backend");
138
+ }
139
+ return subjectId;
140
+ }
141
+
142
+ /**
143
+ * The tenant account of the caller's ALREADY-applied RLS scope. `withRlsContext`
144
+ * sets and read-back-verifies this GUC on the pinned backend before running the
145
+ * scoped body, so it is exact rather than a re-derivation — which lets a seam
146
+ * that only received a workspace id consult
147
+ * {@link subjectHasLiveWorkspaceAuthorityInScope} without a second round trip to
148
+ * resolve the account.
149
+ */
150
+ export async function accountIdInRlsScope(scopedDb: Database): Promise<string> {
151
+ const [row] = await rawRows<{ accountId: string | null }>(
152
+ scopedDb,
153
+ sql`select current_setting('opengeni.account_id', true) as "accountId"`,
154
+ );
155
+ const accountId = row?.accountId ?? "";
156
+ if (!accountId) {
157
+ throw new Error("RLS account context is not applied on the active backend");
158
+ }
159
+ return accountId;
160
+ }
@@ -172,6 +172,55 @@ export const workspaceInstructionPolicyActivationEvents = pgTable(
172
172
  }),
173
173
  );
174
174
 
175
+ export const workspaceInstructionPolicyDeactivationEvents = pgTable(
176
+ "workspace_instruction_policy_deactivation_events",
177
+ {
178
+ id: uuid("id").primaryKey().defaultRandom(),
179
+ operationId: uuid("operation_id").notNull(),
180
+ requestFingerprint: text("request_fingerprint").notNull(),
181
+ accountId: uuid("account_id").notNull(),
182
+ workspaceId: uuid("workspace_id").notNull(),
183
+ kind: text("kind").notNull(),
184
+ scope: text("scope").notNull(),
185
+ roleKey: text("role_key"),
186
+ type: text("type").notNull(),
187
+ activationVersion: bigint("activation_version", { mode: "number" }).notNull(),
188
+ oldRevisionId: uuid("old_revision_id").notNull(),
189
+ oldRevision: bigint("old_revision", { mode: "number" }).notNull(),
190
+ oldContentHash: text("old_content_hash").notNull(),
191
+ actorSubjectId: text("actor_subject_id").notNull(),
192
+ reason: text("reason").notNull(),
193
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
194
+ },
195
+ (table) => ({
196
+ workspaceOperation: uniqueIndex(
197
+ "workspace_instruction_policy_deactivate_workspace_operation_uq",
198
+ ).on(table.workspaceId, table.operationId),
199
+ workspaceActivationVersion: uniqueIndex(
200
+ "workspace_instruction_policy_deactivations_target_version_uq",
201
+ ).on(
202
+ table.workspaceId,
203
+ table.kind,
204
+ table.scope,
205
+ sql`coalesce(${table.roleKey}, '')`,
206
+ table.activationVersion,
207
+ ),
208
+ workspaceTimeline: index("workspace_instruction_policy_deactivations_workspace_time_idx").on(
209
+ table.workspaceId,
210
+ table.createdAt,
211
+ table.id,
212
+ ),
213
+ target: check(
214
+ "workspace_instruction_policy_deactivations_target_chk",
215
+ sql`(
216
+ (${table.kind} = 'charter' and ${table.scope} = 'global' and ${table.roleKey} is null)
217
+ or (${table.kind} = 'policy' and ${table.scope} = 'global' and ${table.roleKey} is null)
218
+ or (${table.kind} = 'policy' and ${table.scope} = 'role' and ${table.roleKey} is not null)
219
+ )`,
220
+ ),
221
+ }),
222
+ );
223
+
175
224
  export const workspaceInstructionPolicyOnboardingProposals = pgTable(
176
225
  "workspace_instruction_policy_onboarding_proposals",
177
226
  {
@@ -211,6 +260,9 @@ export const workspaceInstructionPolicyOnboardingProposals = pgTable(
211
260
  sql`coalesce(${table.roleKey}, '')`,
212
261
  table.sourceId,
213
262
  table.sourceVersion,
263
+ // One proposal per source per baseline: a rebaselined successor is
264
+ // admissible, a duplicate against the same baseline is not.
265
+ table.baselineActivationVersion,
214
266
  ),
215
267
  workspaceTimeline: index(
216
268
  "workspace_instruction_policy_onboarding_proposals_workspace_time_idx",
@@ -239,7 +291,7 @@ export const workspaceInstructionPolicyOnboardingProposals = pgTable(
239
291
  ${table.baselineRevisionId} is null
240
292
  and ${table.baselineRevision} is null
241
293
  and ${table.baselineContentHash} is null
242
- and ${table.baselineActivationVersion} = 0
294
+ and ${table.baselineActivationVersion} >= 0
243
295
  and ${table.baselineActivatedAt} is null
244
296
  ) or (
245
297
  ${table.baselineRevisionId} is not null