@opengeni/db 0.28.9 → 0.36.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 (225) hide show
  1. package/dist/attached-browser-devices.d.ts +42 -0
  2. package/dist/attempt-tool-catalogs.d.ts +16 -0
  3. package/dist/browser-auth.d.ts +299 -0
  4. package/dist/browser-downloads.d.ts +51 -0
  5. package/dist/browser-identities.d.ts +137 -0
  6. package/dist/browser-sessions.d.ts +354 -0
  7. package/dist/browser-state-artifacts.d.ts +91 -0
  8. package/dist/canonical-human-identities.d.ts +48 -0
  9. package/dist/canonical-human-identities.js +28 -0
  10. package/dist/canonical-human-identities.js.map +1 -0
  11. package/dist/capability-components.d.ts +37 -0
  12. package/dist/capability-integrations.d.ts +194 -0
  13. package/dist/chunk-5KTIEDX7.js +13200 -0
  14. package/dist/chunk-5KTIEDX7.js.map +1 -0
  15. package/dist/chunk-CQZ4QSP5.js +995 -0
  16. package/dist/chunk-CQZ4QSP5.js.map +1 -0
  17. package/dist/chunk-F32YT7MP.js +117 -0
  18. package/dist/chunk-F32YT7MP.js.map +1 -0
  19. package/dist/chunk-JC5QPLUM.js +666 -0
  20. package/dist/chunk-JC5QPLUM.js.map +1 -0
  21. package/dist/chunk-P3RE4D2B.js +3535 -0
  22. package/dist/chunk-P3RE4D2B.js.map +1 -0
  23. package/dist/chunk-TAUTWOS5.js +259 -0
  24. package/dist/chunk-TAUTWOS5.js.map +1 -0
  25. package/dist/chunk-VKLB2ZGI.js +2389 -0
  26. package/dist/chunk-VKLB2ZGI.js.map +1 -0
  27. package/dist/codemode-operations.d.ts +117 -0
  28. package/dist/codex-token-resolver.d.ts +2 -2
  29. package/dist/company-profile-schema.d.ts +798 -0
  30. package/dist/company-profile.d.ts +268 -0
  31. package/dist/computer-sessions.d.ts +109 -0
  32. package/dist/connection-token-resolver.d.ts +9 -4
  33. package/dist/database.d.ts +57 -3
  34. package/dist/editable-artifact-durable-export.d.ts +153 -0
  35. package/dist/editable-artifact-durable-export.js +788 -0
  36. package/dist/editable-artifact-durable-export.js.map +1 -0
  37. package/dist/editable-artifact-materialization.d.ts +123 -0
  38. package/dist/editable-artifacts-schema.d.ts +4152 -0
  39. package/dist/editable-artifacts.d.ts +610 -0
  40. package/dist/editable-artifacts.js +26 -0
  41. package/dist/editable-artifacts.js.map +1 -0
  42. package/dist/generated-images.d.ts +139 -0
  43. package/dist/index.d.ts +1122 -126
  44. package/dist/index.js +42329 -18407
  45. package/dist/index.js.map +1 -1
  46. package/dist/insights.d.ts +73 -10
  47. package/dist/integration-bindings.d.ts +70 -0
  48. package/dist/integration-facets.d.ts +99 -0
  49. package/dist/interaction-revisions.d.ts +14 -0
  50. package/dist/interaction-schema.d.ts +6036 -0
  51. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  52. package/dist/knowledge-source-sync.d.ts +274 -0
  53. package/dist/memory-governance.d.ts +2 -2
  54. package/dist/memory-slack-delivery.d.ts +101 -0
  55. package/dist/new-session-drafts.d.ts +2 -2
  56. package/dist/pack-components.d.ts +76 -0
  57. package/dist/pack-installations.d.ts +86 -0
  58. package/dist/persistence-errors.d.ts +3 -3
  59. package/dist/plugin-packages.d.ts +126 -0
  60. package/dist/preference-registry.d.ts +1 -1
  61. package/dist/provision-roles.d.ts +9 -1
  62. package/dist/provision-roles.js +1 -1
  63. package/dist/runtime-posture.d.ts +19 -6
  64. package/dist/schema.d.ts +22654 -9359
  65. package/dist/schema.js +209 -3
  66. package/dist/scoped-knowledge.d.ts +97 -1
  67. package/dist/session-control.d.ts +33 -7
  68. package/dist/session-queue-commands.d.ts +19 -11
  69. package/dist/session-realtime-context.d.ts +2 -2
  70. package/dist/session-realtime-ledger.d.ts +4 -4
  71. package/dist/session-realtime-mirror.d.ts +2 -2
  72. package/dist/session-realtime-state.d.ts +1 -1
  73. package/dist/session-realtime-terminal.d.ts +1 -1
  74. package/dist/session-realtime.d.ts +7 -7
  75. package/dist/session-tenancy.d.ts +44 -0
  76. package/dist/session-tenancy.js +16 -0
  77. package/dist/session-tenancy.js.map +1 -0
  78. package/dist/session-tool-call-settlement.d.ts +1 -1
  79. package/dist/slack-task-policy-schema.d.ts +847 -0
  80. package/dist/slack-task-policy.d.ts +65 -0
  81. package/dist/slack-user-link-access.d.ts +54 -0
  82. package/dist/task-notes-schema.d.ts +924 -0
  83. package/dist/task-notes.d.ts +85 -0
  84. package/dist/transcription-recordings.d.ts +2 -2
  85. package/dist/turn-initiator.d.ts +2 -2
  86. package/dist/video-generation.d.ts +225 -0
  87. package/dist/video-generation.js +68 -0
  88. package/dist/video-generation.js.map +1 -0
  89. package/dist/workspace-artifacts.d.ts +1 -1
  90. package/dist/workspace-instruction-policies.d.ts +1 -1
  91. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  92. package/dist/workspace-learning-policy.d.ts +161 -0
  93. package/dist/xai-subscription.d.ts +326 -0
  94. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  95. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  96. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  97. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  98. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  99. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  100. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  101. package/drizzle/0190_timeline_annotations.sql +31 -0
  102. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  103. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  104. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  105. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  106. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  107. package/drizzle/0196_rig_provider_images.sql +152 -0
  108. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  109. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  110. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  111. package/drizzle/0201_company_profile_authority.sql +849 -0
  112. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  113. package/drizzle/0203_durable_video_generation.sql +353 -0
  114. package/drizzle/0204_video_generation_funding.sql +44 -0
  115. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  116. package/drizzle/0206_browser_sessions.sql +646 -0
  117. package/drizzle/0207_browser_identities.sql +379 -0
  118. package/drizzle/0208_attached_browser_devices.sql +165 -0
  119. package/drizzle/0209_computer_sessions.sql +575 -0
  120. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  121. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  122. package/drizzle/0212_browser_state_transfer_hardening.sql +560 -0
  123. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  124. package/drizzle/0213_browser_interaction_authority.sql +67 -0
  125. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  126. package/drizzle/0214_browser_download_saves.sql +34 -0
  127. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  128. package/drizzle/0215_browser_controller_host.sql +385 -0
  129. package/drizzle/0215_capabilities_platform.sql +980 -0
  130. package/drizzle/0216_browser_auth_health_evidence.sql +137 -0
  131. package/drizzle/0216_pack_component_ownership.sql +235 -0
  132. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  133. package/drizzle/0217_external_browser_auth_operations.sql +22 -0
  134. package/drizzle/0218_organization_tenancy_foundation.sql +299 -0
  135. package/drizzle/0219_organization_tenancy_managed_human_provisioning.sql +317 -0
  136. package/drizzle/0219_site_auth_maintenance_sessions.sql +234 -0
  137. package/drizzle/0220_memory_slack_append_only_cascade.sql +38 -0
  138. package/drizzle/0220_session_channels.sql +54 -0
  139. package/drizzle/0221_sessions_channel_index.sql +5 -0
  140. package/drizzle/0222_session_visibility_authority_epochs.sql +158 -0
  141. package/drizzle/0222_sessions_channel_fk.sql +89 -0
  142. package/drizzle/0223_pending_tool_event_output.sql +55 -0
  143. package/drizzle/0223_sessions_channel_fk_validate.sql +118 -0
  144. package/drizzle/0224_slack_post_outcome_reconciliation.sql +123 -0
  145. package/drizzle/0225_session_visibility_fork_activation.sql +1363 -0
  146. package/drizzle/0226_personal_codex_authority_foundation.sql +365 -0
  147. package/drizzle/0227_slack_native_actions.sql +145 -0
  148. package/drizzle/0228_interaction_controller_data_plane.sql +14 -0
  149. package/drizzle/0228_slack_task_policy.sql +400 -0
  150. package/drizzle/0229_slack_inbox_file_fact.sql +10 -0
  151. package/drizzle/0230_user_scoped_variable_sets_rigs.sql +84 -0
  152. package/drizzle/0231_integration_definition_identity_cutover.sql +271 -0
  153. package/drizzle/0232_integration_facet_authority_cutover.sql +610 -0
  154. package/drizzle/0233_skill_and_integration_authority_cutover.sql +1162 -0
  155. package/drizzle/0234_xai_subscription_authority.sql +1139 -0
  156. package/drizzle/0235_canonical_human_login_bindings.sql +920 -0
  157. package/drizzle/0236_browser_identity_lifecycle.sql +39 -0
  158. package/drizzle/0236_session_visibility_slack_policy.sql +72 -0
  159. package/drizzle/0237_interaction_transition_reaper.sql +263 -0
  160. package/drizzle/0238_goal_persistence_policy.sql +471 -0
  161. package/drizzle/0238_supergrok_realtime_model.sql +29 -0
  162. package/drizzle/0239_supergrok_video_funding.sql +50 -0
  163. package/drizzle/0239_task_tree_notes.sql +990 -0
  164. package/package.json +26 -5
  165. package/src/attached-browser-devices.ts +371 -0
  166. package/src/attempt-tool-catalogs.ts +131 -0
  167. package/src/browser-auth.ts +4373 -0
  168. package/src/browser-downloads.ts +517 -0
  169. package/src/browser-identities.ts +1385 -0
  170. package/src/browser-sessions.ts +2294 -0
  171. package/src/browser-state-artifacts.ts +449 -0
  172. package/src/canonical-human-identities.ts +317 -0
  173. package/src/capability-components.ts +166 -0
  174. package/src/capability-integrations.ts +1571 -0
  175. package/src/codemode-operations.ts +674 -0
  176. package/src/company-profile-schema.ts +143 -0
  177. package/src/company-profile.ts +968 -0
  178. package/src/computer-sessions.ts +1130 -0
  179. package/src/connection-token-resolver.ts +270 -25
  180. package/src/database.ts +468 -24
  181. package/src/editable-artifact-durable-export.ts +1201 -0
  182. package/src/editable-artifact-materialization.ts +893 -0
  183. package/src/editable-artifacts-schema.ts +879 -0
  184. package/src/editable-artifacts.ts +5404 -0
  185. package/src/generated-images.ts +740 -0
  186. package/src/index.ts +9963 -1465
  187. package/src/insights.ts +251 -23
  188. package/src/integration-bindings.ts +455 -0
  189. package/src/integration-facets.ts +862 -0
  190. package/src/interaction-revisions.ts +70 -0
  191. package/src/interaction-schema.ts +1606 -0
  192. package/src/knowledge-source-sync-schema.ts +192 -0
  193. package/src/knowledge-source-sync.ts +1922 -0
  194. package/src/memory-slack-delivery.ts +801 -0
  195. package/src/pack-components.ts +954 -0
  196. package/src/pack-installations.ts +886 -0
  197. package/src/persistence-errors.ts +4 -7
  198. package/src/plugin-packages.ts +1186 -0
  199. package/src/provision-roles.ts +534 -1
  200. package/src/runtime-posture-cli.ts +9 -0
  201. package/src/runtime-posture.ts +721 -5
  202. package/src/schema.ts +6270 -3159
  203. package/src/scoped-knowledge-schema.ts +1 -1
  204. package/src/scoped-knowledge.ts +655 -3
  205. package/src/session-control.ts +130 -42
  206. package/src/session-queue-commands.ts +265 -15
  207. package/src/session-realtime-context.ts +2 -2
  208. package/src/session-realtime-ledger.ts +3 -3
  209. package/src/session-realtime.ts +7 -7
  210. package/src/session-tenancy.ts +188 -0
  211. package/src/session-tool-call-settlement.ts +18 -3
  212. package/src/slack-task-policy-schema.ts +130 -0
  213. package/src/slack-task-policy.ts +341 -0
  214. package/src/slack-user-link-access.ts +702 -0
  215. package/src/task-notes-schema.ts +135 -0
  216. package/src/task-notes.ts +178 -0
  217. package/src/video-generation.ts +1530 -0
  218. package/src/workspace-artifacts.ts +1 -1
  219. package/src/workspace-learning-policy-schema.ts +139 -0
  220. package/src/workspace-learning-policy.ts +595 -0
  221. package/src/xai-subscription.ts +1887 -0
  222. package/dist/chunk-P6CUHXQZ.js +0 -7248
  223. package/dist/chunk-P6CUHXQZ.js.map +0 -1
  224. package/dist/chunk-VHBFHMPC.js +0 -1219
  225. package/dist/chunk-VHBFHMPC.js.map +0 -1
@@ -0,0 +1,188 @@
1
+ import { createHash } from "node:crypto";
2
+ import { sql } from "drizzle-orm";
3
+ import {
4
+ rawRows,
5
+ type Database,
6
+ rlsContextForWorkspace,
7
+ withWorkspaceSubjectRls,
8
+ withWorkspaceSubjectSessionActivityRls,
9
+ } from "./database";
10
+
11
+ export type SessionTenancyVisibility = "user_private" | "workspace_shared";
12
+
13
+ export type TransitionSessionVisibilityInput = {
14
+ workspaceId: string;
15
+ sessionId: string;
16
+ actorSubjectId: string;
17
+ targetVisibility: SessionTenancyVisibility;
18
+ expectedAuthorityEpoch: number;
19
+ operationKey: string;
20
+ };
21
+
22
+ export type TransitionSessionVisibilityResult = {
23
+ operationId: string;
24
+ visibility: SessionTenancyVisibility;
25
+ authorityEpoch: number;
26
+ ownerOrganizationMembershipId: string | null;
27
+ changed: boolean;
28
+ replay: boolean;
29
+ interruptedAttemptCount: number;
30
+ cancelledTurnCount: number;
31
+ cancelledUpdateCount: number;
32
+ pausedGoalCount: number;
33
+ revokedGrantCount: number;
34
+ };
35
+
36
+ export type ForkSessionContentInput = {
37
+ sourceWorkspaceId: string;
38
+ sourceSessionId: string;
39
+ actorSubjectId: string;
40
+ destinationWorkspaceId: string;
41
+ destinationVisibility: SessionTenancyVisibility;
42
+ operationKey: string;
43
+ };
44
+
45
+ export type ForkSessionContentResult = {
46
+ operationId: string;
47
+ sessionId: string;
48
+ workspaceId: string;
49
+ visibility: SessionTenancyVisibility;
50
+ authorityEpoch: number;
51
+ copiedHistoryItemCount: number;
52
+ replay: boolean;
53
+ };
54
+
55
+ export function canonicalSessionVisibilityTransitionHash(
56
+ input: Pick<
57
+ TransitionSessionVisibilityInput,
58
+ "sessionId" | "targetVisibility" | "expectedAuthorityEpoch"
59
+ >,
60
+ ): string {
61
+ return createHash("sha256")
62
+ .update(
63
+ JSON.stringify({
64
+ version: 1,
65
+ sessionId: input.sessionId,
66
+ targetVisibility: input.targetVisibility,
67
+ expectedAuthorityEpoch: input.expectedAuthorityEpoch,
68
+ }),
69
+ )
70
+ .digest("hex");
71
+ }
72
+
73
+ export function canonicalSessionForkHash(
74
+ input: Pick<
75
+ ForkSessionContentInput,
76
+ "sourceSessionId" | "destinationWorkspaceId" | "destinationVisibility"
77
+ >,
78
+ ): string {
79
+ return createHash("sha256")
80
+ .update(
81
+ JSON.stringify({
82
+ version: 1,
83
+ sourceSessionId: input.sourceSessionId,
84
+ destinationWorkspaceId: input.destinationWorkspaceId,
85
+ destinationVisibility: input.destinationVisibility,
86
+ }),
87
+ )
88
+ .digest("hex");
89
+ }
90
+
91
+ export async function transitionSessionVisibility(
92
+ db: Database,
93
+ input: TransitionSessionVisibilityInput,
94
+ ): Promise<TransitionSessionVisibilityResult> {
95
+ if (!Number.isSafeInteger(input.expectedAuthorityEpoch) || input.expectedAuthorityEpoch < 1) {
96
+ throw new Error("expectedAuthorityEpoch must be a positive safe integer");
97
+ }
98
+ if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
99
+ const requestHash = canonicalSessionVisibilityTransitionHash(input);
100
+ const { accountId } = await rlsContextForWorkspace(db, input.workspaceId);
101
+ return await withWorkspaceSubjectSessionActivityRls(
102
+ db,
103
+ input.workspaceId,
104
+ input.actorSubjectId,
105
+ async (scopedDb) => {
106
+ const rows = await rawRows<{
107
+ operationId: string;
108
+ visibility: SessionTenancyVisibility;
109
+ authorityEpoch: number;
110
+ ownerOrganizationMembershipId: string | null;
111
+ changed: boolean;
112
+ replay: boolean;
113
+ interruptedAttemptCount: number;
114
+ cancelledTurnCount: number;
115
+ cancelledUpdateCount: number;
116
+ pausedGoalCount: number;
117
+ revokedGrantCount: number;
118
+ }>(
119
+ scopedDb,
120
+ sql`select
121
+ operation_id as "operationId",
122
+ visibility,
123
+ authority_epoch as "authorityEpoch",
124
+ owner_organization_membership_id as "ownerOrganizationMembershipId",
125
+ changed,
126
+ replay,
127
+ interrupted_attempt_count as "interruptedAttemptCount",
128
+ cancelled_turn_count as "cancelledTurnCount",
129
+ cancelled_update_count as "cancelledUpdateCount",
130
+ paused_goal_count as "pausedGoalCount",
131
+ revoked_grant_count as "revokedGrantCount"
132
+ from transition_session_visibility(
133
+ ${accountId}::uuid,
134
+ ${input.workspaceId}::uuid,
135
+ ${input.sessionId}::uuid,
136
+ ${input.actorSubjectId},
137
+ ${input.targetVisibility},
138
+ ${input.expectedAuthorityEpoch},
139
+ ${input.operationKey},
140
+ ${requestHash}
141
+ )`,
142
+ );
143
+ const result = rows[0];
144
+ if (!result) throw new Error("Session visibility transition returned no result");
145
+ return result;
146
+ },
147
+ );
148
+ }
149
+
150
+ export async function forkSessionContent(
151
+ db: Database,
152
+ input: ForkSessionContentInput,
153
+ ): Promise<ForkSessionContentResult> {
154
+ if (!input.operationKey.trim()) throw new Error("operationKey must not be empty");
155
+ const { accountId } = await rlsContextForWorkspace(db, input.sourceWorkspaceId);
156
+ const requestHash = canonicalSessionForkHash(input);
157
+ return await withWorkspaceSubjectRls(
158
+ db,
159
+ input.sourceWorkspaceId,
160
+ input.actorSubjectId,
161
+ async (scopedDb) => {
162
+ const rows = await rawRows<ForkSessionContentResult>(
163
+ scopedDb,
164
+ sql`select
165
+ operation_id as "operationId",
166
+ session_id as "sessionId",
167
+ workspace_id as "workspaceId",
168
+ visibility,
169
+ authority_epoch as "authorityEpoch",
170
+ copied_history_item_count as "copiedHistoryItemCount",
171
+ replay
172
+ from fork_session_content(
173
+ ${accountId}::uuid,
174
+ ${input.sourceWorkspaceId}::uuid,
175
+ ${input.sourceSessionId}::uuid,
176
+ ${input.actorSubjectId},
177
+ ${input.destinationWorkspaceId}::uuid,
178
+ ${input.destinationVisibility},
179
+ ${input.operationKey},
180
+ ${requestHash}
181
+ )`,
182
+ );
183
+ const result = rows[0];
184
+ if (!result) throw new Error("Session fork returned no result");
185
+ return result;
186
+ },
187
+ );
188
+ }
@@ -4,6 +4,7 @@ import { and, asc, eq, sql } from "drizzle-orm";
4
4
  import type { Database } from "./database";
5
5
  import { fromPostgresLosslessJson, LOSSLESS_CONTENT_CODEC_VERSION } from "./lossless-json";
6
6
  import * as schema from "./schema";
7
+ import { cancelUnacceptedVideoGenerationsForToolCallsInTransaction } from "./video-generation";
7
8
 
8
9
  export const TOOL_RESULT_TYPE_BY_CALL_TYPE: Readonly<Record<string, string>> = {
9
10
  function_call: "function_call_result",
@@ -134,6 +135,10 @@ export async function closePendingSessionToolCallsInTransaction(
134
135
  row.resultItem === null
135
136
  ? null
136
137
  : fromPostgresLosslessJson(row.resultItem, row.resultItemCodecVersion),
138
+ eventOutput:
139
+ row.eventOutput === null
140
+ ? null
141
+ : fromPostgresLosslessJson(row.eventOutput, row.eventOutputCodecVersion),
137
142
  }));
138
143
  if (pending.length === 0) return { sequence: input.sequence, events: [], closed: 0 };
139
144
 
@@ -305,9 +310,11 @@ export async function closePendingSessionToolCallsInTransaction(
305
310
  },
306
311
  ],
307
312
  }
308
- : ((resolution.existingResult?.item ?? resolution.call.resultItem)?.output ??
309
- resolution.existingResult?.item ??
310
- resolution.call.resultItem),
313
+ : resolution.call.eventOutput !== null
314
+ ? resolution.call.eventOutput.value
315
+ : ((resolution.existingResult?.item ?? resolution.call.resultItem)?.output ??
316
+ resolution.existingResult?.item ??
317
+ resolution.call.resultItem),
311
318
  recovery: {
312
319
  interrupted: resolution.interrupted,
313
320
  outcome: resolution.interrupted ? "unknown" : "durable_result_found",
@@ -326,6 +333,14 @@ export async function closePendingSessionToolCallsInTransaction(
326
333
  eventValues.length > 0
327
334
  ? await tx.insert(schema.sessionEvents).values(eventValues).returning()
328
335
  : [];
336
+ await cancelUnacceptedVideoGenerationsForToolCallsInTransaction(tx, {
337
+ workspaceId: input.workspaceId,
338
+ sessionId: input.sessionId,
339
+ turnId: input.turnId,
340
+ toolCallIds: pending.map((call) => call.callId),
341
+ reason: input.reason,
342
+ now: input.now,
343
+ });
329
344
  await tx
330
345
  .delete(schema.sessionPendingToolCalls)
331
346
  .where(
@@ -0,0 +1,130 @@
1
+ import type { SlackTaskPolicyContent } from "@opengeni/contracts";
2
+ import { sql } from "drizzle-orm";
3
+ import {
4
+ bigint,
5
+ check,
6
+ index,
7
+ jsonb,
8
+ pgTable,
9
+ text,
10
+ timestamp,
11
+ uniqueIndex,
12
+ uuid,
13
+ } from "drizzle-orm/pg-core";
14
+
15
+ export const slackTaskPolicyRevisions = pgTable(
16
+ "slack_task_policy_revisions",
17
+ {
18
+ id: uuid("id").primaryKey().defaultRandom(),
19
+ operationId: uuid("operation_id").notNull(),
20
+ requestFingerprint: text("request_fingerprint").notNull(),
21
+ accountId: uuid("account_id").notNull(),
22
+ workspaceId: uuid("workspace_id").notNull(),
23
+ revision: bigint("revision", { mode: "number" })
24
+ .notNull()
25
+ .default(sql`nextval('slack_task_policy_revision_seq')`),
26
+ policy: jsonb("policy").$type<SlackTaskPolicyContent>().notNull(),
27
+ policyHash: text("policy_hash").notNull(),
28
+ supersedesRevisionId: uuid("supersedes_revision_id"),
29
+ createdBySubjectId: text("created_by_subject_id").notNull(),
30
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
31
+ },
32
+ (table) => ({
33
+ workspaceOperation: uniqueIndex("slack_task_policy_revisions_workspace_operation_uq").on(
34
+ table.workspaceId,
35
+ table.operationId,
36
+ ),
37
+ workspaceRevision: uniqueIndex("slack_task_policy_revisions_workspace_revision_uq").on(
38
+ table.workspaceId,
39
+ table.revision,
40
+ ),
41
+ workspaceHistory: index("slack_task_policy_revisions_workspace_history_idx").on(
42
+ table.workspaceId,
43
+ table.revision,
44
+ ),
45
+ receipt: check(
46
+ "slack_task_policy_revisions_receipt_chk",
47
+ sql`${table.requestFingerprint} ~ '^[0-9a-f]{64}$' and ${table.policyHash} ~ '^[0-9a-f]{64}$'`,
48
+ ),
49
+ }),
50
+ );
51
+
52
+ export const slackTaskPolicyHeads = pgTable(
53
+ "slack_task_policy_heads",
54
+ {
55
+ accountId: uuid("account_id").notNull(),
56
+ workspaceId: uuid("workspace_id").primaryKey(),
57
+ revisionId: uuid("revision_id").notNull(),
58
+ revision: bigint("revision", { mode: "number" }).notNull(),
59
+ policyHash: text("policy_hash").notNull(),
60
+ activationVersion: bigint("activation_version", { mode: "number" }).notNull(),
61
+ activatedAt: timestamp("activated_at", { withTimezone: true }).notNull().defaultNow(),
62
+ },
63
+ (table) => ({
64
+ account: index("slack_task_policy_heads_account_idx").on(table.accountId),
65
+ }),
66
+ );
67
+
68
+ export const slackTaskPolicyActivationEvents = pgTable(
69
+ "slack_task_policy_activation_events",
70
+ {
71
+ id: uuid("id").primaryKey().defaultRandom(),
72
+ operationId: uuid("operation_id").notNull(),
73
+ requestFingerprint: text("request_fingerprint").notNull(),
74
+ accountId: uuid("account_id").notNull(),
75
+ workspaceId: uuid("workspace_id").notNull(),
76
+ activationVersion: bigint("activation_version", { mode: "number" }).notNull(),
77
+ oldRevisionId: uuid("old_revision_id"),
78
+ oldRevision: bigint("old_revision", { mode: "number" }),
79
+ oldPolicyHash: text("old_policy_hash"),
80
+ newRevisionId: uuid("new_revision_id").notNull(),
81
+ newRevision: bigint("new_revision", { mode: "number" }).notNull(),
82
+ newPolicyHash: text("new_policy_hash").notNull(),
83
+ actorSubjectId: text("actor_subject_id").notNull(),
84
+ reason: text("reason").notNull(),
85
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
86
+ },
87
+ (table) => ({
88
+ workspaceOperation: uniqueIndex("slack_task_policy_events_workspace_operation_uq").on(
89
+ table.workspaceId,
90
+ table.operationId,
91
+ ),
92
+ workspaceVersion: uniqueIndex("slack_task_policy_events_workspace_version_uq").on(
93
+ table.workspaceId,
94
+ table.activationVersion,
95
+ ),
96
+ workspaceTimeline: index("slack_task_policy_events_workspace_time_idx").on(
97
+ table.workspaceId,
98
+ table.createdAt,
99
+ table.id,
100
+ ),
101
+ }),
102
+ );
103
+
104
+ export const slackSharedTaskOrigins = pgTable(
105
+ "slack_shared_task_origins",
106
+ {
107
+ interactionId: uuid("interaction_id").primaryKey(),
108
+ accountId: uuid("account_id").notNull(),
109
+ workspaceId: uuid("workspace_id").notNull(),
110
+ connectionId: uuid("connection_id").notNull(),
111
+ sessionId: uuid("session_id").notNull(),
112
+ slackTeamId: text("slack_team_id").notNull(),
113
+ sourceChannelId: text("source_channel_id").notNull(),
114
+ sourceThreadTs: text("source_thread_ts").notNull(),
115
+ initiatingSlackUserId: text("initiating_slack_user_id").notNull(),
116
+ policyRevisionId: uuid("policy_revision_id").notNull(),
117
+ policyHash: text("policy_hash").notNull(),
118
+ policyActivationVersion: bigint("policy_activation_version", { mode: "number" }).notNull(),
119
+ publicationMode: text("publication_mode")
120
+ .$type<"never" | "approval_required" | "allow">()
121
+ .notNull(),
122
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
123
+ },
124
+ (table) => ({
125
+ workspaceSession: uniqueIndex("slack_shared_task_origins_workspace_session_uq").on(
126
+ table.workspaceId,
127
+ table.sessionId,
128
+ ),
129
+ }),
130
+ );
@@ -0,0 +1,341 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import {
3
+ SlackTaskPolicyContent,
4
+ canonicalizeSlackTaskPolicy,
5
+ type SlackTaskPolicyActivationEvent,
6
+ type SlackTaskPolicyHead,
7
+ type SlackTaskPolicyListResponse,
8
+ type SlackTaskPolicyMutationResponse,
9
+ type SlackTaskPolicyRevision,
10
+ } from "@opengeni/contracts";
11
+ import { and, desc, eq, sql } from "drizzle-orm";
12
+ import type { Database } from "./database";
13
+ import { rawRows, withRlsContext } from "./database";
14
+ import { nestedPostgresSqlState } from "./persistence-errors";
15
+ import * as schema from "./schema";
16
+
17
+ type RevisionRow = typeof schema.slackTaskPolicyRevisions.$inferSelect;
18
+ type HeadRow = typeof schema.slackTaskPolicyHeads.$inferSelect;
19
+ type EventRow = typeof schema.slackTaskPolicyActivationEvents.$inferSelect;
20
+ type OriginRow = typeof schema.slackSharedTaskOrigins.$inferSelect;
21
+
22
+ export type SlackSharedTaskOrigin = {
23
+ interactionId: string;
24
+ accountId: string;
25
+ workspaceId: string;
26
+ connectionId: string;
27
+ sessionId: string;
28
+ slackTeamId: string;
29
+ sourceChannelId: string;
30
+ sourceThreadTs: string;
31
+ initiatingSlackUserId: string;
32
+ policyRevisionId: string;
33
+ policyHash: string;
34
+ policyActivationVersion: number;
35
+ publicationMode: "never" | "approval_required" | "allow";
36
+ createdAt: Date;
37
+ };
38
+
39
+ export class SlackTaskPolicyConflictError extends Error {
40
+ readonly name = "SlackTaskPolicyConflictError";
41
+ readonly code = "SLACK_TASK_POLICY_CONFLICT";
42
+ constructor(readonly currentHead: SlackTaskPolicyHead | null) {
43
+ super("The active Slack task policy changed in another request");
44
+ }
45
+ }
46
+
47
+ export class SlackTaskPolicyOperationReuseError extends Error {
48
+ readonly name = "SlackTaskPolicyOperationReuseError";
49
+ readonly code = "SLACK_TASK_POLICY_OPERATION_REUSED";
50
+ constructor() {
51
+ super("The Slack task-policy operation id was already used for another request");
52
+ }
53
+ }
54
+
55
+ export class SlackTaskPolicyInvalidOperationError extends Error {
56
+ readonly name = "SlackTaskPolicyInvalidOperationError";
57
+ }
58
+
59
+ export class SlackTaskPolicyAuthorityError extends Error {
60
+ readonly name = "SlackTaskPolicyAuthorityError";
61
+
62
+ constructor(message = "Slack task-policy administration requires exact workspace authority") {
63
+ super(message);
64
+ }
65
+ }
66
+
67
+ function iso(value: Date | string): string {
68
+ return (value instanceof Date ? value : new Date(value)).toISOString();
69
+ }
70
+
71
+ function revisionFromRow(row: RevisionRow): SlackTaskPolicyRevision {
72
+ return {
73
+ id: row.id,
74
+ operationId: row.operationId,
75
+ accountId: row.accountId,
76
+ workspaceId: row.workspaceId,
77
+ revision: row.revision,
78
+ policy: canonicalizeSlackTaskPolicy(SlackTaskPolicyContent.parse(row.policy)),
79
+ policyHash: row.policyHash,
80
+ supersedesRevisionId: row.supersedesRevisionId,
81
+ createdBySubjectId: row.createdBySubjectId,
82
+ createdAt: iso(row.createdAt),
83
+ };
84
+ }
85
+
86
+ function headFromRow(row: HeadRow): SlackTaskPolicyHead {
87
+ return {
88
+ accountId: row.accountId,
89
+ workspaceId: row.workspaceId,
90
+ revisionId: row.revisionId,
91
+ revision: row.revision,
92
+ policyHash: row.policyHash,
93
+ activationVersion: row.activationVersion,
94
+ activatedAt: iso(row.activatedAt),
95
+ };
96
+ }
97
+
98
+ function eventFromRow(row: EventRow): SlackTaskPolicyActivationEvent {
99
+ return {
100
+ id: row.id,
101
+ operationId: row.operationId,
102
+ accountId: row.accountId,
103
+ workspaceId: row.workspaceId,
104
+ activationVersion: row.activationVersion,
105
+ oldRevision:
106
+ row.oldRevisionId === null
107
+ ? null
108
+ : { id: row.oldRevisionId, revision: row.oldRevision!, policyHash: row.oldPolicyHash! },
109
+ newRevision: {
110
+ id: row.newRevisionId,
111
+ revision: row.newRevision,
112
+ policyHash: row.newPolicyHash,
113
+ },
114
+ actorSubjectId: row.actorSubjectId,
115
+ reason: row.reason,
116
+ createdAt: iso(row.createdAt),
117
+ };
118
+ }
119
+
120
+ async function currentSlackTaskPolicyHead(
121
+ db: Database,
122
+ accountId: string,
123
+ workspaceId: string,
124
+ ): Promise<SlackTaskPolicyHead | null> {
125
+ return (await getActiveSlackTaskPolicy(db, { accountId, workspaceId }))?.head ?? null;
126
+ }
127
+
128
+ export async function getActiveSlackTaskPolicy(
129
+ db: Database,
130
+ input: { accountId: string; workspaceId: string },
131
+ ): Promise<{ head: SlackTaskPolicyHead; revision: SlackTaskPolicyRevision } | null> {
132
+ return await withRlsContext(db, input, async (scopedDb) => {
133
+ const [row] = await scopedDb
134
+ .select({
135
+ head: schema.slackTaskPolicyHeads,
136
+ revision: schema.slackTaskPolicyRevisions,
137
+ })
138
+ .from(schema.slackTaskPolicyHeads)
139
+ .innerJoin(
140
+ schema.slackTaskPolicyRevisions,
141
+ and(
142
+ eq(schema.slackTaskPolicyRevisions.id, schema.slackTaskPolicyHeads.revisionId),
143
+ eq(schema.slackTaskPolicyRevisions.workspaceId, schema.slackTaskPolicyHeads.workspaceId),
144
+ ),
145
+ )
146
+ .where(
147
+ and(
148
+ eq(schema.slackTaskPolicyHeads.accountId, input.accountId),
149
+ eq(schema.slackTaskPolicyHeads.workspaceId, input.workspaceId),
150
+ ),
151
+ )
152
+ .limit(1);
153
+ return row ? { head: headFromRow(row.head), revision: revisionFromRow(row.revision) } : null;
154
+ });
155
+ }
156
+
157
+ export async function listSlackTaskPolicy(
158
+ db: Database,
159
+ input: { accountId: string; workspaceId: string; limit?: number },
160
+ ): Promise<SlackTaskPolicyListResponse> {
161
+ return await withRlsContext(db, input, async (scopedDb) => {
162
+ const limit = Math.max(1, Math.min(100, input.limit ?? 50));
163
+ const [active, revisions, events] = await Promise.all([
164
+ getActiveSlackTaskPolicy(scopedDb, input),
165
+ scopedDb
166
+ .select()
167
+ .from(schema.slackTaskPolicyRevisions)
168
+ .where(eq(schema.slackTaskPolicyRevisions.workspaceId, input.workspaceId))
169
+ .orderBy(desc(schema.slackTaskPolicyRevisions.revision))
170
+ .limit(limit),
171
+ scopedDb
172
+ .select()
173
+ .from(schema.slackTaskPolicyActivationEvents)
174
+ .where(eq(schema.slackTaskPolicyActivationEvents.workspaceId, input.workspaceId))
175
+ .orderBy(desc(schema.slackTaskPolicyActivationEvents.activationVersion))
176
+ .limit(limit),
177
+ ]);
178
+ return {
179
+ current: active?.head ?? null,
180
+ activeRevision: active?.revision ?? null,
181
+ revisions: revisions.map(revisionFromRow),
182
+ activationEvents: events.map(eventFromRow),
183
+ };
184
+ });
185
+ }
186
+
187
+ export async function updateSlackTaskPolicy(
188
+ db: Database,
189
+ input: {
190
+ operationId?: string;
191
+ accountId: string;
192
+ workspaceId: string;
193
+ policy: SlackTaskPolicyContent;
194
+ expectedCurrentRevisionId: string | null;
195
+ expectedActivationVersion: number;
196
+ actorSubjectId: string;
197
+ principalKind: "human_session" | undefined;
198
+ reason: string;
199
+ },
200
+ ): Promise<SlackTaskPolicyMutationResponse> {
201
+ if (input.principalKind !== "human_session" || input.actorSubjectId.trim().length === 0) {
202
+ throw new SlackTaskPolicyAuthorityError(
203
+ "Slack task-policy changes require an exact authenticated human actor",
204
+ );
205
+ }
206
+ const operationId = input.operationId ?? randomUUID();
207
+ const policy = canonicalizeSlackTaskPolicy(input.policy);
208
+ const requestFingerprint = createHash("sha256")
209
+ .update(
210
+ JSON.stringify([
211
+ "slack_task_policy_operation",
212
+ 1,
213
+ input.accountId,
214
+ input.workspaceId,
215
+ policy,
216
+ input.expectedCurrentRevisionId,
217
+ input.expectedActivationVersion,
218
+ input.actorSubjectId,
219
+ input.reason,
220
+ ]),
221
+ "utf8",
222
+ )
223
+ .digest("hex");
224
+
225
+ try {
226
+ return await withRlsContext(db, input, async (scopedDb) => {
227
+ await scopedDb.execute(
228
+ sql`select set_config('opengeni.subject_id', ${input.actorSubjectId}, true)`,
229
+ );
230
+ await scopedDb.execute(
231
+ sql`select set_config('opengeni.principal_kind', 'human_session', true)`,
232
+ );
233
+ const [receipt] = await rawRows<{ revisionId: string; eventId: string }>(
234
+ scopedDb,
235
+ sql`select revision_id as "revisionId", event_id as "eventId"
236
+ from slack_task_policy_update(
237
+ ${operationId}::uuid,
238
+ ${requestFingerprint},
239
+ ${input.accountId}::uuid,
240
+ ${input.workspaceId}::uuid,
241
+ ${JSON.stringify(policy)}::jsonb,
242
+ ${input.expectedCurrentRevisionId}::uuid,
243
+ ${input.expectedActivationVersion}::bigint,
244
+ ${input.actorSubjectId},
245
+ ${input.reason}
246
+ )`,
247
+ );
248
+ if (!receipt) throw new Error("Slack task-policy update returned no lifecycle receipt");
249
+ const [[revision], [head], [event]] = await Promise.all([
250
+ scopedDb
251
+ .select()
252
+ .from(schema.slackTaskPolicyRevisions)
253
+ .where(eq(schema.slackTaskPolicyRevisions.id, receipt.revisionId))
254
+ .limit(1),
255
+ scopedDb
256
+ .select()
257
+ .from(schema.slackTaskPolicyHeads)
258
+ .where(eq(schema.slackTaskPolicyHeads.workspaceId, input.workspaceId))
259
+ .limit(1),
260
+ scopedDb
261
+ .select()
262
+ .from(schema.slackTaskPolicyActivationEvents)
263
+ .where(eq(schema.slackTaskPolicyActivationEvents.id, receipt.eventId))
264
+ .limit(1),
265
+ ]);
266
+ if (!revision || !head || !event) {
267
+ throw new Error("Slack task-policy lifecycle receipt was incomplete");
268
+ }
269
+ return {
270
+ revision: revisionFromRow(revision),
271
+ head: headFromRow(head),
272
+ event: eventFromRow(event),
273
+ };
274
+ });
275
+ } catch (error) {
276
+ const state = nestedPostgresSqlState(error);
277
+ if (state === "P1471") throw new SlackTaskPolicyOperationReuseError();
278
+ if (state === "40001") {
279
+ throw new SlackTaskPolicyConflictError(
280
+ await currentSlackTaskPolicyHead(db, input.accountId, input.workspaceId),
281
+ );
282
+ }
283
+ if (state === "42501") {
284
+ throw new SlackTaskPolicyAuthorityError(error instanceof Error ? error.message : undefined);
285
+ }
286
+ if (state === "22023") throw new SlackTaskPolicyInvalidOperationError();
287
+ throw error;
288
+ }
289
+ }
290
+
291
+ function originFromRow(row: OriginRow): SlackSharedTaskOrigin {
292
+ return { ...row };
293
+ }
294
+
295
+ export async function saveSlackSharedTaskOrigin(
296
+ db: Database,
297
+ input: Omit<SlackSharedTaskOrigin, "createdAt">,
298
+ ): Promise<SlackSharedTaskOrigin> {
299
+ return await withRlsContext(db, input, async (scopedDb) => {
300
+ const [created] = await scopedDb
301
+ .insert(schema.slackSharedTaskOrigins)
302
+ .values(input)
303
+ .onConflictDoNothing({ target: schema.slackSharedTaskOrigins.interactionId })
304
+ .returning();
305
+ if (created) return originFromRow(created);
306
+ const [existing] = await scopedDb
307
+ .select()
308
+ .from(schema.slackSharedTaskOrigins)
309
+ .where(eq(schema.slackSharedTaskOrigins.interactionId, input.interactionId))
310
+ .limit(1);
311
+ if (!existing) throw new Error("Slack shared-task origin conflict could not be resolved");
312
+ for (const [key, value] of Object.entries(input)) {
313
+ if (existing[key as keyof OriginRow] !== value) {
314
+ throw new SlackTaskPolicyInvalidOperationError(
315
+ "Slack shared-task origin identity conflicted with immutable evidence",
316
+ );
317
+ }
318
+ }
319
+ return originFromRow(existing);
320
+ });
321
+ }
322
+
323
+ export async function getSlackSharedTaskOrigin(
324
+ db: Database,
325
+ input: { accountId: string; workspaceId: string; interactionId: string },
326
+ ): Promise<SlackSharedTaskOrigin | null> {
327
+ return await withRlsContext(db, input, async (scopedDb) => {
328
+ const [row] = await scopedDb
329
+ .select()
330
+ .from(schema.slackSharedTaskOrigins)
331
+ .where(
332
+ and(
333
+ eq(schema.slackSharedTaskOrigins.accountId, input.accountId),
334
+ eq(schema.slackSharedTaskOrigins.workspaceId, input.workspaceId),
335
+ eq(schema.slackSharedTaskOrigins.interactionId, input.interactionId),
336
+ ),
337
+ )
338
+ .limit(1);
339
+ return row ? originFromRow(row) : null;
340
+ });
341
+ }