@opengeni/db 0.16.2 → 0.17.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.
@@ -0,0 +1,344 @@
1
+ -- deployment-mode: rolling
2
+ -- Durable Slack-to-OpenGeni ingress, identity mapping, thread/session routing,
3
+ -- and delivery cursors. Signed HTTP ingress stores only bounded normalized
4
+ -- fields; raw bodies, headers, signing secrets, bot tokens, and response URLs
5
+ -- never enter these tables.
6
+
7
+ SET LOCAL lock_timeout = '5s';
8
+ SET LOCAL statement_timeout = '10min';
9
+
10
+ CREATE TABLE "slack_bot_user_links" (
11
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
12
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
13
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
14
+ "connection_id" uuid NOT NULL REFERENCES "connections"("id") ON DELETE CASCADE,
15
+ "slack_team_id" text NOT NULL,
16
+ "slack_user_id" text NOT NULL,
17
+ "subject_id" text NOT NULL,
18
+ "linked_by_subject_id" text NOT NULL,
19
+ "created_at" timestamptz NOT NULL DEFAULT now(),
20
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
21
+ CONSTRAINT "slack_bot_user_links_connection_user_uq"
22
+ UNIQUE ("connection_id", "slack_user_id"),
23
+ CONSTRAINT "slack_bot_user_links_identity_check"
24
+ CHECK (
25
+ octet_length("slack_team_id") BETWEEN 1 AND 64
26
+ AND octet_length("slack_user_id") BETWEEN 1 AND 64
27
+ AND octet_length("subject_id") BETWEEN 1 AND 1024
28
+ AND octet_length("linked_by_subject_id") BETWEEN 1 AND 1024
29
+ )
30
+ );
31
+
32
+ CREATE INDEX "slack_bot_user_links_workspace_subject_idx"
33
+ ON "slack_bot_user_links" ("workspace_id", "subject_id");
34
+
35
+ CREATE TABLE "slack_interaction_inbox" (
36
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
37
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
38
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
39
+ "connection_id" uuid NOT NULL REFERENCES "connections"("id") ON DELETE CASCADE,
40
+ "provider_event_id" text NOT NULL,
41
+ "provider_message_id" text NOT NULL,
42
+ "slack_team_id" text NOT NULL,
43
+ "slack_user_id" text NOT NULL,
44
+ "slack_channel_id" text NOT NULL,
45
+ "slack_message_ts" text NOT NULL,
46
+ "slack_thread_ts" text,
47
+ "trigger_kind" text NOT NULL,
48
+ "text" text NOT NULL,
49
+ "status" text NOT NULL DEFAULT 'pending',
50
+ "claim_holder_id" uuid,
51
+ "claim_expires_at" timestamptz,
52
+ "attempt_count" integer NOT NULL DEFAULT 0,
53
+ "last_error_code" text,
54
+ "processed_at" timestamptz,
55
+ "created_at" timestamptz NOT NULL DEFAULT now(),
56
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
57
+ CONSTRAINT "slack_interaction_inbox_provider_event_uq"
58
+ UNIQUE ("connection_id", "provider_event_id"),
59
+ CONSTRAINT "slack_interaction_inbox_provider_message_uq"
60
+ UNIQUE ("connection_id", "provider_message_id"),
61
+ CONSTRAINT "slack_interaction_inbox_bounds_check"
62
+ CHECK (
63
+ octet_length("provider_event_id") BETWEEN 1 AND 256
64
+ AND octet_length("provider_message_id") BETWEEN 1 AND 256
65
+ AND octet_length("slack_team_id") BETWEEN 1 AND 64
66
+ AND octet_length("slack_user_id") BETWEEN 1 AND 64
67
+ AND octet_length("slack_channel_id") BETWEEN 1 AND 64
68
+ AND octet_length("slack_message_ts") BETWEEN 1 AND 64
69
+ AND ("slack_thread_ts" IS NULL OR octet_length("slack_thread_ts") BETWEEN 1 AND 64)
70
+ AND octet_length("text") BETWEEN 1 AND 12000
71
+ AND ("last_error_code" IS NULL OR octet_length("last_error_code") BETWEEN 1 AND 128)
72
+ ),
73
+ CONSTRAINT "slack_interaction_inbox_trigger_check"
74
+ CHECK ("trigger_kind" IN ('app_mention', 'dm', 'slash_command', 'message_shortcut', 'thread_reply')),
75
+ CONSTRAINT "slack_interaction_inbox_status_check"
76
+ CHECK ("status" IN ('pending', 'processing', 'processed', 'failed')),
77
+ CONSTRAINT "slack_interaction_inbox_claim_check"
78
+ CHECK (("claim_holder_id" IS NULL) = ("claim_expires_at" IS NULL)),
79
+ CONSTRAINT "slack_interaction_inbox_completion_check"
80
+ CHECK (("status" IN ('processed', 'failed')) = ("processed_at" IS NOT NULL))
81
+ );
82
+
83
+ CREATE INDEX "slack_interaction_inbox_pending_idx"
84
+ ON "slack_interaction_inbox" ("status", "created_at", "id")
85
+ WHERE "status" IN ('pending', 'processing');
86
+
87
+ CREATE TABLE "slack_interactions" (
88
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
89
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
90
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
91
+ "connection_id" uuid NOT NULL REFERENCES "connections"("id") ON DELETE CASCADE,
92
+ "slack_team_id" text NOT NULL,
93
+ "slack_channel_id" text NOT NULL,
94
+ "slack_thread_ts" text NOT NULL,
95
+ "route_key" text NOT NULL,
96
+ "triggering_provider_event_id" text NOT NULL,
97
+ "owning_subject_id" text NOT NULL,
98
+ "visibility" text NOT NULL,
99
+ "session_reservation_id" uuid NOT NULL DEFAULT gen_random_uuid(),
100
+ "session_id" uuid REFERENCES "sessions"("id") ON DELETE CASCADE,
101
+ "last_delivered_session_event_sequence" integer NOT NULL DEFAULT 0,
102
+ "delivery_claim_holder_id" uuid,
103
+ "delivery_claim_expires_at" timestamptz,
104
+ "ack_slack_message_ts" text,
105
+ "progress_count" integer NOT NULL DEFAULT 0,
106
+ "terminal_delivery_state" text NOT NULL DEFAULT 'open',
107
+ "created_at" timestamptz NOT NULL DEFAULT now(),
108
+ "updated_at" timestamptz NOT NULL DEFAULT now(),
109
+ CONSTRAINT "slack_interactions_route_uq" UNIQUE ("connection_id", "route_key"),
110
+ CONSTRAINT "slack_interactions_identity_uq" UNIQUE ("account_id", "workspace_id", "id"),
111
+ CONSTRAINT "slack_interactions_bounds_check"
112
+ CHECK (
113
+ octet_length("slack_team_id") BETWEEN 1 AND 64
114
+ AND octet_length("slack_channel_id") BETWEEN 1 AND 64
115
+ AND octet_length("slack_thread_ts") BETWEEN 1 AND 64
116
+ AND octet_length("route_key") BETWEEN 1 AND 256
117
+ AND octet_length("triggering_provider_event_id") BETWEEN 1 AND 256
118
+ AND octet_length("owning_subject_id") BETWEEN 1 AND 1024
119
+ AND ("ack_slack_message_ts" IS NULL OR octet_length("ack_slack_message_ts") BETWEEN 1 AND 64)
120
+ AND "last_delivered_session_event_sequence" >= 0
121
+ AND "progress_count" BETWEEN 0 AND 3
122
+ ),
123
+ CONSTRAINT "slack_interactions_session_binding_check"
124
+ CHECK ("session_id" IS NULL OR "session_id" = "session_reservation_id"),
125
+ CONSTRAINT "slack_interactions_delivery_claim_check"
126
+ CHECK (("delivery_claim_holder_id" IS NULL) = ("delivery_claim_expires_at" IS NULL)),
127
+ CONSTRAINT "slack_interactions_visibility_check"
128
+ CHECK ("visibility" IN ('private', 'workspace')),
129
+ CONSTRAINT "slack_interactions_terminal_check"
130
+ CHECK ("terminal_delivery_state" IN ('open', 'completed', 'failed', 'cancelled', 'blocked'))
131
+ );
132
+
133
+ CREATE UNIQUE INDEX "slack_interactions_workspace_reservation_uq"
134
+ ON "slack_interactions" ("workspace_id", "session_reservation_id");
135
+ CREATE UNIQUE INDEX "slack_interactions_workspace_session_uq"
136
+ ON "slack_interactions" ("workspace_id", "session_id")
137
+ WHERE "session_id" IS NOT NULL;
138
+ CREATE INDEX "slack_interactions_delivery_idx"
139
+ ON "slack_interactions" ("terminal_delivery_state", "updated_at", "id")
140
+ WHERE "session_id" IS NOT NULL AND "terminal_delivery_state" = 'open';
141
+
142
+ CREATE TABLE "slack_interaction_progress_deliveries" (
143
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
144
+ "account_id" uuid NOT NULL REFERENCES "managed_accounts"("id") ON DELETE CASCADE,
145
+ "workspace_id" uuid NOT NULL REFERENCES "workspaces"("id") ON DELETE CASCADE,
146
+ "interaction_id" uuid NOT NULL,
147
+ "session_event_sequence" integer NOT NULL,
148
+ "slot" integer NOT NULL,
149
+ "operation_id" uuid NOT NULL,
150
+ "created_at" timestamptz NOT NULL DEFAULT now(),
151
+ CONSTRAINT "slack_interaction_progress_deliveries_interaction_fk"
152
+ FOREIGN KEY ("account_id", "workspace_id", "interaction_id")
153
+ REFERENCES "slack_interactions" ("account_id", "workspace_id", "id")
154
+ ON DELETE CASCADE,
155
+ CONSTRAINT "slack_interaction_progress_deliveries_event_uq"
156
+ UNIQUE ("interaction_id", "session_event_sequence"),
157
+ CONSTRAINT "slack_interaction_progress_deliveries_slot_uq"
158
+ UNIQUE ("interaction_id", "slot"),
159
+ CONSTRAINT "slack_interaction_progress_deliveries_operation_uq"
160
+ UNIQUE ("workspace_id", "operation_id"),
161
+ CONSTRAINT "slack_interaction_progress_deliveries_bounds_check"
162
+ CHECK ("session_event_sequence" > 0 AND "slot" BETWEEN 1 AND 3)
163
+ );
164
+
165
+ ALTER TABLE "slack_bot_user_links" ENABLE ROW LEVEL SECURITY;
166
+ ALTER TABLE "slack_bot_user_links" FORCE ROW LEVEL SECURITY;
167
+ ALTER TABLE "slack_interaction_inbox" ENABLE ROW LEVEL SECURITY;
168
+ ALTER TABLE "slack_interaction_inbox" FORCE ROW LEVEL SECURITY;
169
+ ALTER TABLE "slack_interactions" ENABLE ROW LEVEL SECURITY;
170
+ ALTER TABLE "slack_interactions" FORCE ROW LEVEL SECURITY;
171
+ ALTER TABLE "slack_interaction_progress_deliveries" ENABLE ROW LEVEL SECURITY;
172
+ ALTER TABLE "slack_interaction_progress_deliveries" FORCE ROW LEVEL SECURITY;
173
+
174
+ CREATE POLICY workspace_isolation ON "slack_bot_user_links"
175
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
176
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
177
+ CREATE POLICY workspace_isolation ON "slack_interaction_inbox"
178
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
179
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
180
+ CREATE POLICY workspace_isolation ON "slack_interactions"
181
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
182
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
183
+ CREATE POLICY workspace_isolation ON "slack_interaction_progress_deliveries"
184
+ USING (opengeni_private.workspace_rls_visible("account_id", "workspace_id"))
185
+ WITH CHECK (opengeni_private.workspace_rls_visible("account_id", "workspace_id"));
186
+
187
+ -- Resolve and claim through SECURITY DEFINER without trusting the caller's
188
+ -- search_path (including pg_temp). The migration may target public or a
189
+ -- dedicated embed schema, so bind current_schema() into every relation and
190
+ -- return type at creation time, then expose only pg_catalog at execution time.
191
+ DO $privileged_functions$
192
+ DECLARE data_schema text := current_schema();
193
+ BEGIN
194
+ EXECUTE format($ddl$
195
+ CREATE OR REPLACE FUNCTION opengeni_private.resolve_slack_installation(p_team_id text)
196
+ RETURNS TABLE (
197
+ account_id uuid,
198
+ workspace_id uuid,
199
+ connection_id uuid,
200
+ bot_id text,
201
+ bot_user_id text
202
+ )
203
+ LANGUAGE sql
204
+ SECURITY DEFINER
205
+ SET search_path = pg_catalog
206
+ AS $function$
207
+ WITH eligible AS (
208
+ SELECT
209
+ C.account_id,
210
+ C.workspace_id,
211
+ C.id AS connection_id,
212
+ C.metadata->>'botId' AS bot_id,
213
+ C.metadata->>'botUserId' AS bot_user_id,
214
+ C.created_at
215
+ FROM %1$I.connections C
216
+ WHERE C.provider_domain = 'slack.com'
217
+ AND C.kind = 'app_install'
218
+ AND C.subject_id IS NULL
219
+ AND C.status = 'active'
220
+ AND C.verified_install_at IS NOT NULL
221
+ AND C.verified_install_version = C.version
222
+ AND C.metadata->>'credentialRole' = 'opengeni_slack_bot'
223
+ AND C.metadata->>'slackTeamId' = p_team_id
224
+ AND octet_length(C.metadata->>'botId') BETWEEN 1 AND 64
225
+ AND octet_length(C.metadata->>'botUserId') BETWEEN 1 AND 64
226
+ ), unambiguous AS (
227
+ SELECT count(DISTINCT (workspace_id, bot_id, bot_user_id)) AS principal_count
228
+ FROM eligible
229
+ )
230
+ SELECT E.account_id, E.workspace_id, E.connection_id, E.bot_id, E.bot_user_id
231
+ FROM eligible E, unambiguous U
232
+ WHERE U.principal_count = 1
233
+ ORDER BY E.created_at DESC, E.connection_id DESC
234
+ LIMIT 1
235
+ $function$
236
+ $ddl$, data_schema);
237
+
238
+ EXECUTE format($ddl$
239
+ CREATE OR REPLACE FUNCTION opengeni_private.claim_slack_interaction_inbox(
240
+ p_holder uuid,
241
+ p_lease_ms integer
242
+ )
243
+ RETURNS SETOF %1$I.slack_interaction_inbox
244
+ LANGUAGE plpgsql
245
+ SECURITY DEFINER
246
+ SET search_path = pg_catalog
247
+ AS $function$
248
+ BEGIN
249
+ IF p_lease_ms < 1000 OR p_lease_ms > 300000 THEN
250
+ RAISE EXCEPTION 'invalid Slack inbox claim lease';
251
+ END IF;
252
+ RETURN QUERY
253
+ WITH candidate AS (
254
+ SELECT I.id
255
+ FROM %1$I.slack_interaction_inbox I
256
+ WHERE I.status = 'pending'
257
+ OR (I.status = 'processing' AND I.claim_expires_at <= now())
258
+ ORDER BY I.created_at, I.id
259
+ FOR UPDATE SKIP LOCKED
260
+ LIMIT 1
261
+ )
262
+ UPDATE %1$I.slack_interaction_inbox I
263
+ SET status = 'processing',
264
+ claim_holder_id = p_holder,
265
+ claim_expires_at = now() + make_interval(secs => p_lease_ms::double precision / 1000),
266
+ attempt_count = I.attempt_count + 1,
267
+ updated_at = now()
268
+ FROM candidate C
269
+ WHERE I.id = C.id
270
+ RETURNING I.*;
271
+ END
272
+ $function$
273
+ $ddl$, data_schema);
274
+
275
+ EXECUTE format($ddl$
276
+ CREATE OR REPLACE FUNCTION opengeni_private.claim_slack_interaction_delivery(
277
+ p_holder uuid,
278
+ p_lease_ms integer
279
+ )
280
+ RETURNS SETOF %1$I.slack_interactions
281
+ LANGUAGE plpgsql
282
+ SECURITY DEFINER
283
+ SET search_path = pg_catalog
284
+ AS $function$
285
+ BEGIN
286
+ IF p_lease_ms < 1000 OR p_lease_ms > 300000 THEN
287
+ RAISE EXCEPTION 'invalid Slack delivery claim lease';
288
+ END IF;
289
+ RETURN QUERY
290
+ WITH candidate AS (
291
+ SELECT I.id
292
+ FROM %1$I.slack_interactions I
293
+ WHERE I.session_id IS NOT NULL
294
+ AND I.terminal_delivery_state = 'open'
295
+ AND (I.delivery_claim_holder_id IS NULL OR I.delivery_claim_expires_at <= now())
296
+ AND EXISTS (
297
+ SELECT 1
298
+ FROM %1$I.session_events E
299
+ WHERE E.workspace_id = I.workspace_id
300
+ AND E.session_id = I.session_id
301
+ AND E.sequence > I.last_delivered_session_event_sequence
302
+ AND E.type IN (
303
+ 'agent.message.completed',
304
+ 'session.humanInput.requested',
305
+ 'turn.completed',
306
+ 'turn.failed',
307
+ 'turn.cancelled',
308
+ 'session.status.changed'
309
+ )
310
+ )
311
+ ORDER BY I.updated_at, I.id
312
+ FOR UPDATE SKIP LOCKED
313
+ LIMIT 1
314
+ )
315
+ UPDATE %1$I.slack_interactions I
316
+ SET delivery_claim_holder_id = p_holder,
317
+ delivery_claim_expires_at = now() + make_interval(secs => p_lease_ms::double precision / 1000),
318
+ updated_at = now()
319
+ FROM candidate C
320
+ WHERE I.id = C.id
321
+ RETURNING I.*;
322
+ END
323
+ $function$
324
+ $ddl$, data_schema);
325
+ END
326
+ $privileged_functions$;
327
+
328
+ DO $grants$
329
+ DECLARE target_schema text := current_schema();
330
+ BEGIN
331
+ IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'opengeni_app') THEN
332
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.slack_bot_user_links TO opengeni_app', target_schema);
333
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.slack_interaction_inbox TO opengeni_app', target_schema);
334
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.slack_interactions TO opengeni_app', target_schema);
335
+ EXECUTE format('GRANT SELECT, INSERT, UPDATE, DELETE ON TABLE %I.slack_interaction_progress_deliveries TO opengeni_app', target_schema);
336
+ GRANT EXECUTE ON FUNCTION opengeni_private.resolve_slack_installation(text) TO opengeni_app;
337
+ GRANT EXECUTE ON FUNCTION opengeni_private.claim_slack_interaction_inbox(uuid, integer) TO opengeni_app;
338
+ GRANT EXECUTE ON FUNCTION opengeni_private.claim_slack_interaction_delivery(uuid, integer) TO opengeni_app;
339
+ END IF;
340
+ END
341
+ $grants$;
342
+
343
+ RESET statement_timeout;
344
+ RESET lock_timeout;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@opengeni/db",
3
- "version": "0.16.2",
3
+ "version": "0.17.1",
4
4
  "description": "OpenGeni persistence: Drizzle schema, RLS-scoped query layer, the SQL migration runner, and role provisioning.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -51,8 +51,8 @@
51
51
  },
52
52
  "dependencies": {
53
53
  "@opengeni/codex": "^0.2.9",
54
- "@opengeni/config": "^0.7.22",
55
- "@opengeni/contracts": "^0.26.1",
54
+ "@opengeni/config": "^0.8.1",
55
+ "@opengeni/contracts": "^0.27.0",
56
56
  "@opengeni/network": "^0.1.1",
57
57
  "drizzle-orm": "^0.45.2",
58
58
  "postgres": "^3.4.7"