@opengeni/db 0.28.1 → 0.31.1

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 (187) 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 +91 -0
  4. package/dist/browser-identities.d.ts +128 -0
  5. package/dist/browser-sessions.d.ts +272 -0
  6. package/dist/browser-state-artifacts.d.ts +17 -0
  7. package/dist/capability-components.d.ts +37 -0
  8. package/dist/capability-integrations.d.ts +192 -0
  9. package/dist/chunk-F5FT7BEZ.js +3535 -0
  10. package/dist/chunk-F5FT7BEZ.js.map +1 -0
  11. package/dist/chunk-ISI2TIUG.js +639 -0
  12. package/dist/chunk-ISI2TIUG.js.map +1 -0
  13. package/dist/chunk-MWDVXQOL.js +11639 -0
  14. package/dist/chunk-MWDVXQOL.js.map +1 -0
  15. package/dist/chunk-NBE6CHRI.js +955 -0
  16. package/dist/chunk-NBE6CHRI.js.map +1 -0
  17. package/dist/{chunk-DBS5HPEW.js → chunk-YHZXQ7HP.js} +760 -11
  18. package/dist/chunk-YHZXQ7HP.js.map +1 -0
  19. package/dist/codemode-operations.d.ts +117 -0
  20. package/dist/codex-token-resolver.d.ts +2 -2
  21. package/dist/company-profile-schema.d.ts +798 -0
  22. package/dist/company-profile.d.ts +268 -0
  23. package/dist/computer-sessions.d.ts +109 -0
  24. package/dist/connection-token-resolver.d.ts +9 -4
  25. package/dist/database.d.ts +52 -3
  26. package/dist/editable-artifact-durable-export.d.ts +153 -0
  27. package/dist/editable-artifact-durable-export.js +788 -0
  28. package/dist/editable-artifact-durable-export.js.map +1 -0
  29. package/dist/editable-artifact-materialization.d.ts +123 -0
  30. package/dist/editable-artifacts-schema.d.ts +4152 -0
  31. package/dist/editable-artifacts.d.ts +610 -0
  32. package/dist/editable-artifacts.js +26 -0
  33. package/dist/editable-artifacts.js.map +1 -0
  34. package/dist/environment-crypto.d.ts +8 -4
  35. package/dist/generated-images.d.ts +139 -0
  36. package/dist/index.d.ts +958 -126
  37. package/dist/index.js +31204 -12172
  38. package/dist/index.js.map +1 -1
  39. package/dist/insights.d.ts +51 -4
  40. package/dist/integration-bindings.d.ts +70 -0
  41. package/dist/integration-features.d.ts +99 -0
  42. package/dist/interaction-revisions.d.ts +3 -0
  43. package/dist/interaction-schema.d.ts +5372 -0
  44. package/dist/knowledge-source-sync-schema.d.ts +1679 -0
  45. package/dist/knowledge-source-sync.d.ts +274 -0
  46. package/dist/lossless-columns.d.ts +58 -0
  47. package/dist/lossless-json.d.ts +39 -0
  48. package/dist/memory-domain.d.ts +3 -6
  49. package/dist/memory-governance.d.ts +2 -2
  50. package/dist/memory-slack-delivery.d.ts +101 -0
  51. package/dist/new-session-drafts.d.ts +2 -2
  52. package/dist/pack-components.d.ts +76 -0
  53. package/dist/pack-installations.d.ts +86 -0
  54. package/dist/persistence-errors.d.ts +7 -14
  55. package/dist/plugin-packages.d.ts +126 -0
  56. package/dist/preference-registry.d.ts +1 -1
  57. package/dist/provision-roles.d.ts +9 -1
  58. package/dist/provision-roles.js +1 -1
  59. package/dist/runtime-posture.d.ts +18 -6
  60. package/dist/schema.d.ts +19122 -8731
  61. package/dist/schema.js +173 -1
  62. package/dist/scoped-knowledge.d.ts +97 -1
  63. package/dist/session-control.d.ts +4 -4
  64. package/dist/session-queue-commands.d.ts +19 -11
  65. package/dist/session-realtime-context.d.ts +2 -2
  66. package/dist/session-realtime-ledger.d.ts +4 -4
  67. package/dist/session-realtime-mirror.d.ts +2 -2
  68. package/dist/session-realtime-state.d.ts +1 -1
  69. package/dist/session-realtime-terminal.d.ts +8 -1
  70. package/dist/session-realtime.d.ts +7 -7
  71. package/dist/session-tool-call-settlement.d.ts +1 -1
  72. package/dist/slack-user-link-access.d.ts +54 -0
  73. package/dist/transcription-recordings-schema.d.ts +44 -6
  74. package/dist/transcription-recordings.d.ts +3 -3
  75. package/dist/turn-initiator.d.ts +2 -2
  76. package/dist/video-generation.d.ts +216 -0
  77. package/dist/video-generation.js +66 -0
  78. package/dist/video-generation.js.map +1 -0
  79. package/dist/workspace-artifacts.d.ts +1 -1
  80. package/dist/workspace-instruction-policies.d.ts +1 -1
  81. package/dist/workspace-learning-policy-schema.d.ts +932 -0
  82. package/dist/workspace-learning-policy.d.ts +161 -0
  83. package/drizzle/0065_enrollment_credential_generation.sql +10 -0
  84. package/drizzle/0140_retained_screenshot_artifacts.sql +192 -0
  85. package/drizzle/0176_lossless_canonical_json.sql +975 -0
  86. package/drizzle/0177_session_events_workspace_turn_type_index.sql +5 -0
  87. package/drizzle/0178_permissioned_secret_reads.sql +14 -0
  88. package/drizzle/0179_slack_private_shortcut_delivery_gate.sql +85 -0
  89. package/drizzle/0180_retained_screenshot_lifecycle_fences.sql +273 -0
  90. package/drizzle/0181_connected_machine_removal.sql +52 -0
  91. package/drizzle/0182_connected_machine_remove_session_default.sql +77 -0
  92. package/drizzle/0183_model_call_provider_cost_estimates.sql +39 -0
  93. package/drizzle/0184_sandbox_drain_teardown_fence.sql +682 -0
  94. package/drizzle/0185_temporal_schedule_cleanup_outbox.sql +168 -0
  95. package/drizzle/0186_sandbox_capture_provider_contract.sql +400 -0
  96. package/drizzle/0187_generated_image_artifacts.sql +173 -0
  97. package/drizzle/0188_image_generation_retention_failure.sql +25 -0
  98. package/drizzle/0189_retained_session_image_formats.sql +13 -0
  99. package/drizzle/0190_timeline_annotations.sql +31 -0
  100. package/drizzle/0191_editable_artifact_engine.sql +3853 -0
  101. package/drizzle/0192_editable_artifact_live_tickets.sql +338 -0
  102. package/drizzle/0193_editable_artifact_authorization.sql +190 -0
  103. package/drizzle/0194_editable_artifact_durable_exports.sql +160 -0
  104. package/drizzle/0195_editable_artifact_import_authorization.sql +203 -0
  105. package/drizzle/0196_rig_provider_images.sql +152 -0
  106. package/drizzle/0197_knowledge_source_sync_schedules.sql +523 -0
  107. package/drizzle/0198_memory_slack_publication_delivery.sql +393 -0
  108. package/drizzle/0199_workspace_learning_policy.sql +980 -0
  109. package/drizzle/0201_company_profile_authority.sql +849 -0
  110. package/drizzle/0202_document_index_checkpoints.sql +85 -0
  111. package/drizzle/0203_durable_video_generation.sql +353 -0
  112. package/drizzle/0204_video_generation_funding.sql +44 -0
  113. package/drizzle/0205_attempt_tool_catalogs.sql +230 -0
  114. package/drizzle/0206_browser_sessions.sql +646 -0
  115. package/drizzle/0207_browser_identities.sql +379 -0
  116. package/drizzle/0208_attached_browser_devices.sql +165 -0
  117. package/drizzle/0209_computer_sessions.sql +575 -0
  118. package/drizzle/0210_browser_auth_network_interventions.sql +441 -0
  119. package/drizzle/0211_editable_artifact_session_links.sql +58 -0
  120. package/drizzle/0212_slack_installation_bindings.sql +348 -0
  121. package/drizzle/0213_slack_user_link_access_requests.sql +177 -0
  122. package/drizzle/0214_session_activity_commit_gate.sql +262 -0
  123. package/drizzle/0215_capabilities_platform.sql +980 -0
  124. package/drizzle/0216_pack_component_ownership.sql +235 -0
  125. package/drizzle/0217_capability_definition_delete_authority.sql +32 -0
  126. package/package.json +18 -5
  127. package/src/attached-browser-devices.ts +371 -0
  128. package/src/attempt-tool-catalogs.ts +131 -0
  129. package/src/browser-auth.ts +1896 -0
  130. package/src/browser-identities.ts +1168 -0
  131. package/src/browser-sessions.ts +2005 -0
  132. package/src/browser-state-artifacts.ts +59 -0
  133. package/src/capability-components.ts +166 -0
  134. package/src/capability-integrations.ts +1663 -0
  135. package/src/codemode-operations.ts +674 -0
  136. package/src/company-profile-schema.ts +143 -0
  137. package/src/company-profile.ts +968 -0
  138. package/src/computer-sessions.ts +1130 -0
  139. package/src/connection-token-resolver.ts +270 -25
  140. package/src/database.ts +434 -24
  141. package/src/editable-artifact-durable-export.ts +1201 -0
  142. package/src/editable-artifact-materialization.ts +893 -0
  143. package/src/editable-artifacts-schema.ts +879 -0
  144. package/src/editable-artifacts.ts +5404 -0
  145. package/src/environment-crypto.ts +22 -9
  146. package/src/generated-images.ts +740 -0
  147. package/src/index.ts +10117 -2730
  148. package/src/insights.ts +160 -13
  149. package/src/integration-bindings.ts +455 -0
  150. package/src/integration-features.ts +859 -0
  151. package/src/interaction-revisions.ts +40 -0
  152. package/src/interaction-schema.ts +1303 -0
  153. package/src/knowledge-source-sync-schema.ts +192 -0
  154. package/src/knowledge-source-sync.ts +1922 -0
  155. package/src/lossless-columns.ts +35 -0
  156. package/src/lossless-json.ts +322 -0
  157. package/src/memory-domain.ts +5 -44
  158. package/src/memory-slack-delivery.ts +801 -0
  159. package/src/pack-components.ts +954 -0
  160. package/src/pack-installations.ts +929 -0
  161. package/src/persistence-errors.ts +26 -34
  162. package/src/plugin-packages.ts +1186 -0
  163. package/src/provision-roles.ts +302 -1
  164. package/src/runtime-posture-cli.ts +9 -0
  165. package/src/runtime-posture.ts +514 -5
  166. package/src/schema.ts +5177 -2957
  167. package/src/scoped-knowledge-schema.ts +1 -1
  168. package/src/scoped-knowledge.ts +655 -3
  169. package/src/session-control.ts +133 -81
  170. package/src/session-queue-commands.ts +511 -247
  171. package/src/session-realtime-context.ts +20 -7
  172. package/src/session-realtime-ledger.ts +32 -10
  173. package/src/session-realtime-mirror.ts +4 -2
  174. package/src/session-realtime-terminal.ts +89 -21
  175. package/src/session-realtime.ts +10 -9
  176. package/src/session-tool-call-settlement.ts +38 -15
  177. package/src/slack-user-link-access.ts +702 -0
  178. package/src/transcription-recordings-schema.ts +5 -2
  179. package/src/transcription-recordings.ts +70 -40
  180. package/src/video-generation.ts +1452 -0
  181. package/src/workspace-learning-policy-schema.ts +139 -0
  182. package/src/workspace-learning-policy.ts +595 -0
  183. package/dist/chunk-DBS5HPEW.js.map +0 -1
  184. package/dist/chunk-HT5T62CC.js +0 -6803
  185. package/dist/chunk-HT5T62CC.js.map +0 -1
  186. package/dist/event-payload-sanitizer.d.ts +0 -32
  187. package/src/event-payload-sanitizer.ts +0 -377
@@ -0,0 +1,702 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import {
4
+ type Permission,
5
+ SlackUserLinkAccessRequest as SlackUserLinkAccessRequestSchema,
6
+ type SlackUserLinkAccessRequest,
7
+ type SlackUserLinkAccessRequestStatus,
8
+ } from "@opengeni/contracts";
9
+ import { and, asc, eq, gt, inArray, ne } from "drizzle-orm";
10
+
11
+ import { type Database, withWorkspaceRls } from "./database";
12
+ import { withLosslessContentWriteVersion } from "./lossless-json";
13
+ import * as schema from "./schema";
14
+
15
+ type RequestRow = typeof schema.slackUserLinkAccessRequests.$inferSelect;
16
+ type OperationKind = "request" | "cancel" | "approve" | "deny";
17
+
18
+ export class SlackUserLinkAccessPersistenceError extends Error {
19
+ constructor(
20
+ public readonly code:
21
+ | "not_found"
22
+ | "subject_mismatch"
23
+ | "claim_mismatch"
24
+ | "version_conflict"
25
+ | "state_conflict"
26
+ | "idempotency_conflict"
27
+ | "slack_identity_conflict",
28
+ ) {
29
+ super(code);
30
+ this.name = "SlackUserLinkAccessPersistenceError";
31
+ }
32
+ }
33
+
34
+ export type PrepareSlackUserLinkAccessInput = {
35
+ accountId: string;
36
+ workspaceId: string;
37
+ tokenDigest: string;
38
+ connectionId: string;
39
+ slackTeamId: string;
40
+ slackUserId: string;
41
+ subjectId: string;
42
+ subjectLabel?: string | null;
43
+ expiresAt: Date;
44
+ };
45
+
46
+ export type SlackUserLinkAccessMutationInput = {
47
+ workspaceId: string;
48
+ requestId: string;
49
+ actorSubjectId: string;
50
+ expectedVersion: number;
51
+ idempotencyKey: string;
52
+ };
53
+
54
+ export type ApproveSlackUserLinkAccessInput = SlackUserLinkAccessMutationInput & {
55
+ role?: string;
56
+ permissions: Permission[];
57
+ };
58
+
59
+ const activeStatuses: SlackUserLinkAccessRequestStatus[] = ["prepared", "pending"];
60
+
61
+ function isActiveStatus(status: string): status is (typeof activeStatuses)[number] {
62
+ return activeStatuses.includes(status as (typeof activeStatuses)[number]);
63
+ }
64
+
65
+ /**
66
+ * Persist one verified Slack bearer as token-free, subject-bound state. A
67
+ * second fresh bearer for the same exact subject/provider principal supersedes
68
+ * only that subject's older active intent; another subject's row is untouched.
69
+ */
70
+ export async function prepareSlackUserLinkAccessRequest(
71
+ db: Database,
72
+ input: PrepareSlackUserLinkAccessInput,
73
+ now = new Date(),
74
+ ): Promise<SlackUserLinkAccessRequest> {
75
+ return await withWorkspaceRls(
76
+ db,
77
+ input.workspaceId,
78
+ async (scopedDb) =>
79
+ await scopedDb.transaction(async (tx) => {
80
+ const database = tx as unknown as Database;
81
+ const [existing] = await tx
82
+ .select()
83
+ .from(schema.slackUserLinkAccessRequests)
84
+ .where(eq(schema.slackUserLinkAccessRequests.tokenDigest, input.tokenDigest))
85
+ .for("update")
86
+ .limit(1);
87
+ if (existing) {
88
+ assertExactClaims(existing, input);
89
+ if (existing.subjectId !== input.subjectId) {
90
+ throw new SlackUserLinkAccessPersistenceError("subject_mismatch");
91
+ }
92
+ return mapRequest(await expireLockedRequest(database, existing, now));
93
+ }
94
+
95
+ const superseded = await tx
96
+ .select()
97
+ .from(schema.slackUserLinkAccessRequests)
98
+ .where(
99
+ and(
100
+ eq(schema.slackUserLinkAccessRequests.workspaceId, input.workspaceId),
101
+ eq(schema.slackUserLinkAccessRequests.connectionId, input.connectionId),
102
+ eq(schema.slackUserLinkAccessRequests.slackUserId, input.slackUserId),
103
+ eq(schema.slackUserLinkAccessRequests.subjectId, input.subjectId),
104
+ inArray(schema.slackUserLinkAccessRequests.status, activeStatuses),
105
+ ne(schema.slackUserLinkAccessRequests.tokenDigest, input.tokenDigest),
106
+ ),
107
+ )
108
+ .for("update");
109
+ for (const row of superseded) {
110
+ const current = await expireLockedRequest(database, row, now);
111
+ if (!isActiveStatus(current.status)) continue;
112
+ const [cancelled] = await tx
113
+ .update(schema.slackUserLinkAccessRequests)
114
+ .set({
115
+ status: "cancelled",
116
+ version: current.version + 1,
117
+ decidedAt: now,
118
+ decisionBySubjectId: input.subjectId,
119
+ updatedAt: now,
120
+ })
121
+ .where(
122
+ and(
123
+ eq(schema.slackUserLinkAccessRequests.id, current.id),
124
+ eq(schema.slackUserLinkAccessRequests.version, current.version),
125
+ ),
126
+ )
127
+ .returning();
128
+ if (cancelled) {
129
+ await insertAudit(database, cancelled, input.subjectId, "slack.user_link.superseded");
130
+ }
131
+ }
132
+
133
+ const [created] = await tx
134
+ .insert(schema.slackUserLinkAccessRequests)
135
+ .values({
136
+ accountId: input.accountId,
137
+ workspaceId: input.workspaceId,
138
+ tokenDigest: input.tokenDigest,
139
+ connectionId: input.connectionId,
140
+ slackTeamId: input.slackTeamId,
141
+ slackUserId: input.slackUserId,
142
+ subjectId: input.subjectId,
143
+ subjectLabel: input.subjectLabel ?? null,
144
+ expiresAt: input.expiresAt,
145
+ })
146
+ .returning();
147
+ if (!created) throw new Error("Slack user-link access request insert returned no row");
148
+ await insertAudit(database, created, input.subjectId, "slack.user_link.prepared");
149
+ return mapRequest(created);
150
+ }),
151
+ { isolationLevel: "serializable" },
152
+ );
153
+ }
154
+
155
+ export async function getSlackUserLinkAccessRequestForSubject(
156
+ db: Database,
157
+ input: { workspaceId: string; requestId: string; subjectId: string },
158
+ now = new Date(),
159
+ ): Promise<SlackUserLinkAccessRequest | null> {
160
+ return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
161
+ return await scopedDb.transaction(async (tx) => {
162
+ const [row] = await tx
163
+ .select()
164
+ .from(schema.slackUserLinkAccessRequests)
165
+ .where(
166
+ and(
167
+ eq(schema.slackUserLinkAccessRequests.workspaceId, input.workspaceId),
168
+ eq(schema.slackUserLinkAccessRequests.id, input.requestId),
169
+ eq(schema.slackUserLinkAccessRequests.subjectId, input.subjectId),
170
+ ),
171
+ )
172
+ .for("update")
173
+ .limit(1);
174
+ if (!row) return null;
175
+ return mapRequest(await expireLockedRequest(tx as unknown as Database, row, now));
176
+ });
177
+ });
178
+ }
179
+
180
+ export async function requestSlackUserLinkWorkspaceAccess(
181
+ db: Database,
182
+ input: SlackUserLinkAccessMutationInput,
183
+ now = new Date(),
184
+ ): Promise<SlackUserLinkAccessRequest> {
185
+ return await mutateRequest(db, input, "request", {}, now, async (database, row) => {
186
+ if (row.subjectId !== input.actorSubjectId) {
187
+ throw new SlackUserLinkAccessPersistenceError("subject_mismatch");
188
+ }
189
+ if (row.status !== "prepared") {
190
+ throw new SlackUserLinkAccessPersistenceError("state_conflict");
191
+ }
192
+ const [updated] = await database
193
+ .update(schema.slackUserLinkAccessRequests)
194
+ .set({
195
+ status: "pending",
196
+ version: row.version + 1,
197
+ requestedAt: now,
198
+ updatedAt: now,
199
+ })
200
+ .where(
201
+ and(
202
+ eq(schema.slackUserLinkAccessRequests.id, row.id),
203
+ eq(schema.slackUserLinkAccessRequests.version, row.version),
204
+ ),
205
+ )
206
+ .returning();
207
+ if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
208
+ await insertAudit(database, updated, input.actorSubjectId, "workspace.access_request.created");
209
+ return updated;
210
+ });
211
+ }
212
+
213
+ export async function cancelSlackUserLinkAccessRequest(
214
+ db: Database,
215
+ input: SlackUserLinkAccessMutationInput,
216
+ now = new Date(),
217
+ ): Promise<SlackUserLinkAccessRequest> {
218
+ return await mutateRequest(db, input, "cancel", {}, now, async (database, row) => {
219
+ if (row.subjectId !== input.actorSubjectId) {
220
+ throw new SlackUserLinkAccessPersistenceError("subject_mismatch");
221
+ }
222
+ if (row.status !== "prepared" && row.status !== "pending") {
223
+ throw new SlackUserLinkAccessPersistenceError("state_conflict");
224
+ }
225
+ const [updated] = await database
226
+ .update(schema.slackUserLinkAccessRequests)
227
+ .set({
228
+ status: "cancelled",
229
+ version: row.version + 1,
230
+ decidedAt: now,
231
+ decisionBySubjectId: input.actorSubjectId,
232
+ updatedAt: now,
233
+ })
234
+ .where(
235
+ and(
236
+ eq(schema.slackUserLinkAccessRequests.id, row.id),
237
+ eq(schema.slackUserLinkAccessRequests.version, row.version),
238
+ ),
239
+ )
240
+ .returning();
241
+ if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
242
+ await insertAudit(
243
+ database,
244
+ updated,
245
+ input.actorSubjectId,
246
+ "workspace.access_request.cancelled",
247
+ );
248
+ return updated;
249
+ });
250
+ }
251
+
252
+ export async function denySlackUserLinkAccessRequest(
253
+ db: Database,
254
+ input: SlackUserLinkAccessMutationInput,
255
+ now = new Date(),
256
+ ): Promise<SlackUserLinkAccessRequest> {
257
+ return await mutateRequest(db, input, "deny", {}, now, async (database, row) => {
258
+ if (row.status !== "pending") {
259
+ throw new SlackUserLinkAccessPersistenceError("state_conflict");
260
+ }
261
+ const [updated] = await database
262
+ .update(schema.slackUserLinkAccessRequests)
263
+ .set({
264
+ status: "denied",
265
+ version: row.version + 1,
266
+ decidedAt: now,
267
+ decisionBySubjectId: input.actorSubjectId,
268
+ updatedAt: now,
269
+ })
270
+ .where(
271
+ and(
272
+ eq(schema.slackUserLinkAccessRequests.id, row.id),
273
+ eq(schema.slackUserLinkAccessRequests.version, row.version),
274
+ ),
275
+ )
276
+ .returning();
277
+ if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
278
+ await insertAudit(database, updated, input.actorSubjectId, "workspace.access_request.denied");
279
+ return updated;
280
+ });
281
+ }
282
+
283
+ export async function approveSlackUserLinkAccessRequest(
284
+ db: Database,
285
+ input: ApproveSlackUserLinkAccessInput,
286
+ now = new Date(),
287
+ ): Promise<SlackUserLinkAccessRequest> {
288
+ return await mutateRequest(
289
+ db,
290
+ input,
291
+ "approve",
292
+ { role: input.role ?? "member", permissions: input.permissions },
293
+ now,
294
+ async (database, row) => {
295
+ if (row.status !== "pending") {
296
+ throw new SlackUserLinkAccessPersistenceError("state_conflict");
297
+ }
298
+ await assertSlackIdentityAvailable(database, row);
299
+ const role = input.role ?? "member";
300
+ await database
301
+ .insert(schema.workspaceMemberships)
302
+ .values({
303
+ accountId: row.accountId,
304
+ workspaceId: row.workspaceId,
305
+ subjectId: row.subjectId,
306
+ subjectLabel: row.subjectLabel,
307
+ role,
308
+ permissions: input.permissions,
309
+ })
310
+ .onConflictDoUpdate({
311
+ target: [schema.workspaceMemberships.subjectId, schema.workspaceMemberships.workspaceId],
312
+ set: {
313
+ subjectLabel: row.subjectLabel,
314
+ role,
315
+ permissions: input.permissions,
316
+ updatedAt: now,
317
+ },
318
+ });
319
+ const membership = await membershipAllowsSlackLink(database, row);
320
+ if (!membership) throw new Error("Slack access approval did not create the required grant");
321
+ await upsertSlackIdentityLink(database, row, input.actorSubjectId, now);
322
+ const [updated] = await database
323
+ .update(schema.slackUserLinkAccessRequests)
324
+ .set({
325
+ status: "completed",
326
+ version: row.version + 1,
327
+ decidedAt: now,
328
+ decisionBySubjectId: input.actorSubjectId,
329
+ approvedRole: role,
330
+ approvedPermissions: input.permissions,
331
+ completedAt: now,
332
+ updatedAt: now,
333
+ })
334
+ .where(
335
+ and(
336
+ eq(schema.slackUserLinkAccessRequests.id, row.id),
337
+ eq(schema.slackUserLinkAccessRequests.version, row.version),
338
+ ),
339
+ )
340
+ .returning();
341
+ if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
342
+ await insertAudit(
343
+ database,
344
+ updated,
345
+ input.actorSubjectId,
346
+ "workspace.access_request.approved",
347
+ {
348
+ membershipRole: role,
349
+ membershipPermissions: input.permissions,
350
+ },
351
+ );
352
+ await insertAudit(database, updated, input.actorSubjectId, "slack.user_link.completed");
353
+ return updated;
354
+ },
355
+ );
356
+ }
357
+
358
+ /**
359
+ * Complete a prepared/pending link only when the subject's live membership
360
+ * already grants sessions:create (or workspace:admin). This is used both for
361
+ * ordinary already-authorized links and after a separate canonical member grant.
362
+ */
363
+ export async function completeSlackUserLinkAccessIfGranted(
364
+ db: Database,
365
+ input: { workspaceId: string; requestId: string; subjectId: string },
366
+ now = new Date(),
367
+ ): Promise<SlackUserLinkAccessRequest | null> {
368
+ return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
369
+ return await scopedDb.transaction(async (tx) => {
370
+ const database = tx as unknown as Database;
371
+ const [found] = await tx
372
+ .select()
373
+ .from(schema.slackUserLinkAccessRequests)
374
+ .where(
375
+ and(
376
+ eq(schema.slackUserLinkAccessRequests.workspaceId, input.workspaceId),
377
+ eq(schema.slackUserLinkAccessRequests.id, input.requestId),
378
+ eq(schema.slackUserLinkAccessRequests.subjectId, input.subjectId),
379
+ ),
380
+ )
381
+ .for("update")
382
+ .limit(1);
383
+ if (!found) return null;
384
+ const row = await expireLockedRequest(database, found, now);
385
+ if (row.status === "completed" || !isActiveStatus(row.status)) {
386
+ return mapRequest(row);
387
+ }
388
+ if (!(await membershipAllowsSlackLink(database, row))) {
389
+ return mapRequest(row);
390
+ }
391
+ await assertSlackIdentityAvailable(database, row);
392
+ await upsertSlackIdentityLink(database, row, input.subjectId, now);
393
+ const [updated] = await tx
394
+ .update(schema.slackUserLinkAccessRequests)
395
+ .set({
396
+ status: "completed",
397
+ version: row.version + 1,
398
+ decidedAt: now,
399
+ decisionBySubjectId: input.subjectId,
400
+ completedAt: now,
401
+ updatedAt: now,
402
+ })
403
+ .where(
404
+ and(
405
+ eq(schema.slackUserLinkAccessRequests.id, row.id),
406
+ eq(schema.slackUserLinkAccessRequests.version, row.version),
407
+ ),
408
+ )
409
+ .returning();
410
+ if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
411
+ await insertAudit(database, updated, input.subjectId, "slack.user_link.completed");
412
+ return mapRequest(updated);
413
+ });
414
+ });
415
+ }
416
+
417
+ export async function listPendingSlackUserLinkAccessRequests(
418
+ db: Database,
419
+ workspaceId: string,
420
+ now = new Date(),
421
+ ): Promise<SlackUserLinkAccessRequest[]> {
422
+ return await withWorkspaceRls(db, workspaceId, async (scopedDb) => {
423
+ return await scopedDb.transaction(async (tx) => {
424
+ const database = tx as unknown as Database;
425
+ const expired = await tx
426
+ .select()
427
+ .from(schema.slackUserLinkAccessRequests)
428
+ .where(
429
+ and(
430
+ eq(schema.slackUserLinkAccessRequests.workspaceId, workspaceId),
431
+ inArray(schema.slackUserLinkAccessRequests.status, activeStatuses),
432
+ ),
433
+ )
434
+ .for("update");
435
+ for (const row of expired) {
436
+ if (row.expiresAt.getTime() <= now.getTime()) {
437
+ await expireLockedRequest(database, row, now);
438
+ }
439
+ }
440
+ const rows = await tx
441
+ .select()
442
+ .from(schema.slackUserLinkAccessRequests)
443
+ .where(
444
+ and(
445
+ eq(schema.slackUserLinkAccessRequests.workspaceId, workspaceId),
446
+ eq(schema.slackUserLinkAccessRequests.status, "pending"),
447
+ gt(schema.slackUserLinkAccessRequests.expiresAt, now),
448
+ ),
449
+ )
450
+ .orderBy(asc(schema.slackUserLinkAccessRequests.requestedAt));
451
+ return rows.map(mapRequest);
452
+ });
453
+ });
454
+ }
455
+
456
+ async function mutateRequest(
457
+ db: Database,
458
+ input: SlackUserLinkAccessMutationInput,
459
+ operation: OperationKind,
460
+ protectedInput: Record<string, unknown>,
461
+ now: Date,
462
+ transition: (database: Database, row: RequestRow) => Promise<RequestRow>,
463
+ ): Promise<SlackUserLinkAccessRequest> {
464
+ return await withWorkspaceRls(db, input.workspaceId, async (scopedDb) => {
465
+ return await scopedDb.transaction(async (tx) => {
466
+ const database = tx as unknown as Database;
467
+ const requestDigest = digestOperation({
468
+ operation,
469
+ requestId: input.requestId,
470
+ actorSubjectId: input.actorSubjectId,
471
+ expectedVersion: input.expectedVersion,
472
+ ...protectedInput,
473
+ });
474
+ const [found] = await tx
475
+ .select()
476
+ .from(schema.slackUserLinkAccessRequests)
477
+ .where(
478
+ and(
479
+ eq(schema.slackUserLinkAccessRequests.workspaceId, input.workspaceId),
480
+ eq(schema.slackUserLinkAccessRequests.id, input.requestId),
481
+ ),
482
+ )
483
+ .for("update")
484
+ .limit(1);
485
+ if (!found) throw new SlackUserLinkAccessPersistenceError("not_found");
486
+ const [replay] = await tx
487
+ .select()
488
+ .from(schema.slackUserLinkAccessRequestOperations)
489
+ .where(
490
+ and(
491
+ eq(schema.slackUserLinkAccessRequestOperations.requestId, input.requestId),
492
+ eq(schema.slackUserLinkAccessRequestOperations.actorSubjectId, input.actorSubjectId),
493
+ eq(schema.slackUserLinkAccessRequestOperations.operation, operation),
494
+ eq(schema.slackUserLinkAccessRequestOperations.idempotencyKey, input.idempotencyKey),
495
+ ),
496
+ )
497
+ .limit(1);
498
+ if (replay) {
499
+ if (
500
+ replay.requestDigest !== requestDigest ||
501
+ replay.expectedVersion !== input.expectedVersion
502
+ ) {
503
+ throw new SlackUserLinkAccessPersistenceError("idempotency_conflict");
504
+ }
505
+ return SlackUserLinkAccessRequestSchema.parse(replay.result);
506
+ }
507
+ const row = await expireLockedRequest(database, found, now);
508
+ if (row.version !== input.expectedVersion) {
509
+ throw new SlackUserLinkAccessPersistenceError("version_conflict");
510
+ }
511
+ const updated = await transition(database, row);
512
+ const result = mapRequest(updated);
513
+ await tx.insert(schema.slackUserLinkAccessRequestOperations).values({
514
+ accountId: updated.accountId,
515
+ workspaceId: updated.workspaceId,
516
+ requestId: updated.id,
517
+ actorSubjectId: input.actorSubjectId,
518
+ operation,
519
+ idempotencyKey: input.idempotencyKey,
520
+ requestDigest,
521
+ expectedVersion: input.expectedVersion,
522
+ resultVersion: updated.version,
523
+ resultStatus: updated.status,
524
+ result,
525
+ });
526
+ return result;
527
+ });
528
+ });
529
+ }
530
+
531
+ async function expireLockedRequest(database: Database, row: RequestRow, now: Date) {
532
+ if (!isActiveStatus(row.status) || row.expiresAt.getTime() > now.getTime()) {
533
+ return row;
534
+ }
535
+ const [updated] = await database
536
+ .update(schema.slackUserLinkAccessRequests)
537
+ .set({
538
+ status: "expired",
539
+ version: row.version + 1,
540
+ decidedAt: now,
541
+ updatedAt: now,
542
+ })
543
+ .where(
544
+ and(
545
+ eq(schema.slackUserLinkAccessRequests.id, row.id),
546
+ eq(schema.slackUserLinkAccessRequests.version, row.version),
547
+ ),
548
+ )
549
+ .returning();
550
+ if (!updated) throw new SlackUserLinkAccessPersistenceError("version_conflict");
551
+ await insertAudit(database, updated, null, "workspace.access_request.expired");
552
+ return updated;
553
+ }
554
+
555
+ async function membershipAllowsSlackLink(database: Database, row: RequestRow) {
556
+ const [membership] = await database
557
+ .select({ permissions: schema.workspaceMemberships.permissions })
558
+ .from(schema.workspaceMemberships)
559
+ .where(
560
+ and(
561
+ eq(schema.workspaceMemberships.workspaceId, row.workspaceId),
562
+ eq(schema.workspaceMemberships.subjectId, row.subjectId),
563
+ ),
564
+ )
565
+ .limit(1);
566
+ const permissions = membership?.permissions ?? [];
567
+ return permissions.includes("sessions:create") || permissions.includes("workspace:admin");
568
+ }
569
+
570
+ async function assertSlackIdentityAvailable(database: Database, row: RequestRow) {
571
+ const [existing] = await database
572
+ .select({ subjectId: schema.slackBotUserLinks.subjectId })
573
+ .from(schema.slackBotUserLinks)
574
+ .where(
575
+ and(
576
+ eq(schema.slackBotUserLinks.connectionId, row.connectionId),
577
+ eq(schema.slackBotUserLinks.slackUserId, row.slackUserId),
578
+ ),
579
+ )
580
+ .for("update")
581
+ .limit(1);
582
+ if (existing && existing.subjectId !== row.subjectId) {
583
+ throw new SlackUserLinkAccessPersistenceError("slack_identity_conflict");
584
+ }
585
+ }
586
+
587
+ async function upsertSlackIdentityLink(
588
+ database: Database,
589
+ row: RequestRow,
590
+ actorSubjectId: string,
591
+ now: Date,
592
+ ) {
593
+ const [created] = await database
594
+ .insert(schema.slackBotUserLinks)
595
+ .values({
596
+ accountId: row.accountId,
597
+ workspaceId: row.workspaceId,
598
+ connectionId: row.connectionId,
599
+ slackTeamId: row.slackTeamId,
600
+ slackUserId: row.slackUserId,
601
+ subjectId: row.subjectId,
602
+ linkedBySubjectId: actorSubjectId,
603
+ })
604
+ .onConflictDoNothing({
605
+ target: [schema.slackBotUserLinks.connectionId, schema.slackBotUserLinks.slackUserId],
606
+ })
607
+ .returning({ id: schema.slackBotUserLinks.id });
608
+ if (created) return;
609
+
610
+ const [existing] = await database
611
+ .select({ id: schema.slackBotUserLinks.id, subjectId: schema.slackBotUserLinks.subjectId })
612
+ .from(schema.slackBotUserLinks)
613
+ .where(
614
+ and(
615
+ eq(schema.slackBotUserLinks.connectionId, row.connectionId),
616
+ eq(schema.slackBotUserLinks.slackUserId, row.slackUserId),
617
+ ),
618
+ )
619
+ .for("update")
620
+ .limit(1);
621
+ if (!existing || existing.subjectId !== row.subjectId) {
622
+ throw new SlackUserLinkAccessPersistenceError("slack_identity_conflict");
623
+ }
624
+ await database
625
+ .update(schema.slackBotUserLinks)
626
+ .set({
627
+ accountId: row.accountId,
628
+ workspaceId: row.workspaceId,
629
+ slackTeamId: row.slackTeamId,
630
+ linkedBySubjectId: actorSubjectId,
631
+ updatedAt: now,
632
+ })
633
+ .where(
634
+ and(
635
+ eq(schema.slackBotUserLinks.id, existing.id),
636
+ eq(schema.slackBotUserLinks.subjectId, row.subjectId),
637
+ ),
638
+ );
639
+ }
640
+
641
+ async function insertAudit(
642
+ database: Database,
643
+ row: RequestRow,
644
+ actorSubjectId: string | null,
645
+ action: string,
646
+ metadata: Record<string, unknown> = {},
647
+ ) {
648
+ await database.insert(schema.auditEvents).values(
649
+ withLosslessContentWriteVersion(
650
+ {
651
+ accountId: row.accountId,
652
+ workspaceId: row.workspaceId,
653
+ subjectId: actorSubjectId,
654
+ action,
655
+ targetType: "slack_user_link_access_request",
656
+ targetId: row.id,
657
+ metadata: {
658
+ status: row.status,
659
+ version: row.version,
660
+ connectionId: row.connectionId,
661
+ ...metadata,
662
+ },
663
+ },
664
+ "metadata",
665
+ "metadataCodecVersion",
666
+ ),
667
+ );
668
+ }
669
+
670
+ function assertExactClaims(row: RequestRow, input: PrepareSlackUserLinkAccessInput) {
671
+ if (
672
+ row.accountId !== input.accountId ||
673
+ row.workspaceId !== input.workspaceId ||
674
+ row.connectionId !== input.connectionId ||
675
+ row.slackTeamId !== input.slackTeamId ||
676
+ row.slackUserId !== input.slackUserId ||
677
+ row.expiresAt.getTime() !== input.expiresAt.getTime()
678
+ ) {
679
+ throw new SlackUserLinkAccessPersistenceError("claim_mismatch");
680
+ }
681
+ }
682
+
683
+ function digestOperation(value: Record<string, unknown>) {
684
+ return createHash("sha256").update(JSON.stringify(value)).digest("hex");
685
+ }
686
+
687
+ function mapRequest(row: RequestRow): SlackUserLinkAccessRequest {
688
+ return {
689
+ id: row.id,
690
+ workspaceId: row.workspaceId,
691
+ workspaceDisplayName: null,
692
+ subjectLabel: row.subjectLabel,
693
+ status: row.status as SlackUserLinkAccessRequestStatus,
694
+ version: row.version,
695
+ expiresAt: row.expiresAt.toISOString(),
696
+ requestedAt: row.requestedAt?.toISOString() ?? null,
697
+ decidedAt: row.decidedAt?.toISOString() ?? null,
698
+ completedAt: row.completedAt?.toISOString() ?? null,
699
+ createdAt: row.createdAt.toISOString(),
700
+ updatedAt: row.updatedAt.toISOString(),
701
+ };
702
+ }