@realtimex/folio 0.1.16 → 0.1.17

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 (47) hide show
  1. package/api/src/middleware/auth.ts +77 -0
  2. package/api/src/routes/chat.ts +7 -1
  3. package/api/src/routes/index.ts +2 -0
  4. package/api/src/routes/ingestions.ts +45 -5
  5. package/api/src/routes/policies.ts +50 -7
  6. package/api/src/routes/stats.ts +9 -5
  7. package/api/src/routes/workspaces.ts +290 -0
  8. package/api/src/services/ChatService.ts +8 -2
  9. package/api/src/services/IngestionService.ts +38 -26
  10. package/api/src/services/PolicyEngine.ts +4 -1
  11. package/api/src/services/PolicyLearningService.ts +31 -6
  12. package/api/src/services/PolicyLoader.ts +44 -25
  13. package/api/src/services/RAGService.ts +52 -12
  14. package/dist/api/src/middleware/auth.js +59 -0
  15. package/dist/api/src/routes/chat.js +1 -1
  16. package/dist/api/src/routes/index.js +2 -0
  17. package/dist/api/src/routes/ingestions.js +45 -8
  18. package/dist/api/src/routes/policies.js +49 -7
  19. package/dist/api/src/routes/stats.js +9 -5
  20. package/dist/api/src/routes/workspaces.js +220 -0
  21. package/dist/api/src/services/ChatService.js +7 -2
  22. package/dist/api/src/services/IngestionService.js +35 -30
  23. package/dist/api/src/services/PolicyEngine.js +2 -1
  24. package/dist/api/src/services/PolicyLearningService.js +28 -6
  25. package/dist/api/src/services/PolicyLoader.js +29 -25
  26. package/dist/api/src/services/RAGService.js +43 -11
  27. package/dist/assets/index-CTn5FcC4.js +113 -0
  28. package/dist/assets/index-Dq9sxoZK.css +1 -0
  29. package/dist/index.html +2 -2
  30. package/package.json +1 -1
  31. package/supabase/functions/workspace-invite/index.ts +110 -0
  32. package/supabase/migrations/20260223000000_initial_foundation.sql +5 -0
  33. package/supabase/migrations/20260224000004_add_avatars_storage.sql +4 -0
  34. package/supabase/migrations/20260224000006_add_policies_table.sql +5 -0
  35. package/supabase/migrations/20260224000008_add_ingestions_table.sql +2 -0
  36. package/supabase/migrations/20260225000000_setup_compatible_mode.sql +17 -4
  37. package/supabase/migrations/20260225000003_add_baseline_configs.sql +4 -3
  38. package/supabase/migrations/20260226000000_add_processing_events.sql +1 -0
  39. package/supabase/migrations/20260226000002_add_dynamic_rag.sql +1 -0
  40. package/supabase/migrations/20260226000005_add_chat_tables.sql +3 -0
  41. package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +4 -0
  42. package/supabase/migrations/20260302064608_add_ingestion_llm_settings_compat.sql +15 -0
  43. package/supabase/migrations/20260303000000_add_workspaces_phase1.sql +459 -0
  44. package/supabase/migrations/20260303010000_add_workspace_management_rpc.sql +310 -0
  45. package/supabase/migrations/20260303020000_workspace_scope_document_chunks.sql +139 -0
  46. package/dist/assets/index-DzN8-j-e.css +0 -1
  47. package/dist/assets/index-dnBz6SWG.js +0 -113
@@ -0,0 +1,310 @@
1
+ -- Workspace member management RPC helpers.
2
+ -- These SECURITY DEFINER functions provide controlled cross-member management
3
+ -- while keeping table-level RLS strict.
4
+
5
+ create or replace function public.workspace_list_members(
6
+ p_workspace_id uuid
7
+ )
8
+ returns table (
9
+ user_id uuid,
10
+ role text,
11
+ status text,
12
+ joined_at timestamptz,
13
+ first_name text,
14
+ last_name text,
15
+ email text,
16
+ avatar_url text,
17
+ is_current_user boolean
18
+ )
19
+ language plpgsql
20
+ security definer
21
+ set search_path = 'public', 'auth', 'pg_catalog'
22
+ as $$
23
+ declare
24
+ requester_id uuid := auth.uid();
25
+ requester_role text;
26
+ begin
27
+ if requester_id is null then
28
+ raise exception 'Authentication required' using errcode = '42501';
29
+ end if;
30
+
31
+ select wm.role
32
+ into requester_role
33
+ from public.workspace_members wm
34
+ where wm.workspace_id = p_workspace_id
35
+ and wm.user_id = requester_id
36
+ and wm.status = 'active'
37
+ limit 1;
38
+
39
+ if requester_role is null then
40
+ raise exception 'Workspace membership required' using errcode = '42501';
41
+ end if;
42
+
43
+ return query
44
+ select
45
+ wm.user_id,
46
+ wm.role,
47
+ wm.status,
48
+ wm.created_at as joined_at,
49
+ p.first_name,
50
+ p.last_name,
51
+ coalesce(p.email, u.email) as email,
52
+ p.avatar_url,
53
+ (wm.user_id = requester_id) as is_current_user
54
+ from public.workspace_members wm
55
+ left join public.profiles p
56
+ on p.id = wm.user_id
57
+ left join auth.users u
58
+ on u.id = wm.user_id
59
+ where wm.workspace_id = p_workspace_id
60
+ order by
61
+ case wm.role
62
+ when 'owner' then 0
63
+ when 'admin' then 1
64
+ else 2
65
+ end,
66
+ wm.created_at,
67
+ wm.user_id;
68
+ end;
69
+ $$;
70
+
71
+ grant execute on function public.workspace_list_members(uuid) to authenticated;
72
+
73
+ create or replace function public.workspace_invite_member(
74
+ p_workspace_id uuid,
75
+ p_email text,
76
+ p_role text default 'member'
77
+ )
78
+ returns table (
79
+ user_id uuid,
80
+ role text,
81
+ status text
82
+ )
83
+ language plpgsql
84
+ security definer
85
+ set search_path = 'public', 'auth', 'pg_catalog'
86
+ as $$
87
+ declare
88
+ requester_id uuid := auth.uid();
89
+ requester_role text;
90
+ target_email text := lower(trim(coalesce(p_email, '')));
91
+ target_user_id uuid;
92
+ existing_role text;
93
+ begin
94
+ if requester_id is null then
95
+ raise exception 'Authentication required' using errcode = '42501';
96
+ end if;
97
+
98
+ if p_role not in ('admin', 'member') then
99
+ raise exception 'Invalid role. Expected admin or member.' using errcode = '22023';
100
+ end if;
101
+
102
+ if target_email = '' then
103
+ raise exception 'Email is required' using errcode = '22023';
104
+ end if;
105
+
106
+ select wm.role
107
+ into requester_role
108
+ from public.workspace_members wm
109
+ where wm.workspace_id = p_workspace_id
110
+ and wm.user_id = requester_id
111
+ and wm.status = 'active'
112
+ limit 1;
113
+
114
+ if requester_role not in ('owner', 'admin') then
115
+ raise exception 'Only workspace admins can invite members' using errcode = '42501';
116
+ end if;
117
+
118
+ if p_role = 'admin' and requester_role <> 'owner' then
119
+ raise exception 'Only workspace owners can invite admins' using errcode = '42501';
120
+ end if;
121
+
122
+ select u.id
123
+ into target_user_id
124
+ from auth.users u
125
+ where lower(coalesce(u.email, '')) = target_email
126
+ limit 1;
127
+
128
+ if target_user_id is null then
129
+ raise exception 'No user found with that email. They must sign up first.' using errcode = '22023';
130
+ end if;
131
+
132
+ select wm.role
133
+ into existing_role
134
+ from public.workspace_members wm
135
+ where wm.workspace_id = p_workspace_id
136
+ and wm.user_id = target_user_id
137
+ limit 1;
138
+
139
+ if existing_role = 'owner' then
140
+ raise exception 'Owner membership cannot be modified' using errcode = '42501';
141
+ end if;
142
+
143
+ if requester_role <> 'owner' and existing_role = 'admin' then
144
+ raise exception 'Only workspace owners can modify admin memberships' using errcode = '42501';
145
+ end if;
146
+
147
+ if existing_role is null then
148
+ insert into public.workspace_members (workspace_id, user_id, role, status)
149
+ values (p_workspace_id, target_user_id, p_role, 'active');
150
+ else
151
+ update public.workspace_members
152
+ set
153
+ role = p_role,
154
+ status = 'active',
155
+ updated_at = now()
156
+ where workspace_id = p_workspace_id
157
+ and user_id = target_user_id;
158
+ end if;
159
+
160
+ return query
161
+ select wm.user_id, wm.role, wm.status
162
+ from public.workspace_members wm
163
+ where wm.workspace_id = p_workspace_id
164
+ and wm.user_id = target_user_id
165
+ limit 1;
166
+ end;
167
+ $$;
168
+
169
+ grant execute on function public.workspace_invite_member(uuid, text, text) to authenticated;
170
+
171
+ create or replace function public.workspace_update_member_role(
172
+ p_workspace_id uuid,
173
+ p_target_user_id uuid,
174
+ p_role text
175
+ )
176
+ returns table (
177
+ user_id uuid,
178
+ role text,
179
+ status text
180
+ )
181
+ language plpgsql
182
+ security definer
183
+ set search_path = 'public', 'auth', 'pg_catalog'
184
+ as $$
185
+ declare
186
+ requester_id uuid := auth.uid();
187
+ requester_role text;
188
+ target_role text;
189
+ begin
190
+ if requester_id is null then
191
+ raise exception 'Authentication required' using errcode = '42501';
192
+ end if;
193
+
194
+ if p_role not in ('admin', 'member') then
195
+ raise exception 'Invalid role. Expected admin or member.' using errcode = '22023';
196
+ end if;
197
+
198
+ select wm.role
199
+ into requester_role
200
+ from public.workspace_members wm
201
+ where wm.workspace_id = p_workspace_id
202
+ and wm.user_id = requester_id
203
+ and wm.status = 'active'
204
+ limit 1;
205
+
206
+ if requester_role not in ('owner', 'admin') then
207
+ raise exception 'Only workspace admins can manage roles' using errcode = '42501';
208
+ end if;
209
+
210
+ if p_role = 'admin' and requester_role <> 'owner' then
211
+ raise exception 'Only workspace owners can promote admins' using errcode = '42501';
212
+ end if;
213
+
214
+ select wm.role
215
+ into target_role
216
+ from public.workspace_members wm
217
+ where wm.workspace_id = p_workspace_id
218
+ and wm.user_id = p_target_user_id
219
+ and wm.status = 'active'
220
+ limit 1;
221
+
222
+ if target_role is null then
223
+ raise exception 'Workspace member not found' using errcode = '22023';
224
+ end if;
225
+
226
+ if target_role = 'owner' then
227
+ raise exception 'Owner membership cannot be modified' using errcode = '42501';
228
+ end if;
229
+
230
+ if requester_role <> 'owner' and target_role = 'admin' then
231
+ raise exception 'Only workspace owners can modify admin roles' using errcode = '42501';
232
+ end if;
233
+
234
+ update public.workspace_members
235
+ set
236
+ role = p_role,
237
+ updated_at = now()
238
+ where workspace_id = p_workspace_id
239
+ and user_id = p_target_user_id;
240
+
241
+ return query
242
+ select wm.user_id, wm.role, wm.status
243
+ from public.workspace_members wm
244
+ where wm.workspace_id = p_workspace_id
245
+ and wm.user_id = p_target_user_id
246
+ limit 1;
247
+ end;
248
+ $$;
249
+
250
+ grant execute on function public.workspace_update_member_role(uuid, uuid, text) to authenticated;
251
+
252
+ create or replace function public.workspace_remove_member(
253
+ p_workspace_id uuid,
254
+ p_target_user_id uuid
255
+ )
256
+ returns boolean
257
+ language plpgsql
258
+ security definer
259
+ set search_path = 'public', 'auth', 'pg_catalog'
260
+ as $$
261
+ declare
262
+ requester_id uuid := auth.uid();
263
+ requester_role text;
264
+ target_role text;
265
+ begin
266
+ if requester_id is null then
267
+ raise exception 'Authentication required' using errcode = '42501';
268
+ end if;
269
+
270
+ select wm.role
271
+ into requester_role
272
+ from public.workspace_members wm
273
+ where wm.workspace_id = p_workspace_id
274
+ and wm.user_id = requester_id
275
+ and wm.status = 'active'
276
+ limit 1;
277
+
278
+ if requester_role not in ('owner', 'admin') then
279
+ raise exception 'Only workspace admins can remove members' using errcode = '42501';
280
+ end if;
281
+
282
+ select wm.role
283
+ into target_role
284
+ from public.workspace_members wm
285
+ where wm.workspace_id = p_workspace_id
286
+ and wm.user_id = p_target_user_id
287
+ and wm.status = 'active'
288
+ limit 1;
289
+
290
+ if target_role is null then
291
+ return false;
292
+ end if;
293
+
294
+ if target_role = 'owner' then
295
+ raise exception 'Owner membership cannot be removed' using errcode = '42501';
296
+ end if;
297
+
298
+ if requester_role <> 'owner' and target_role = 'admin' then
299
+ raise exception 'Only workspace owners can remove admins' using errcode = '42501';
300
+ end if;
301
+
302
+ delete from public.workspace_members
303
+ where workspace_id = p_workspace_id
304
+ and user_id = p_target_user_id;
305
+
306
+ return found;
307
+ end;
308
+ $$;
309
+
310
+ grant execute on function public.workspace_remove_member(uuid, uuid) to authenticated;
@@ -0,0 +1,139 @@
1
+ -- Workspace-scope RAG chunks and workspace-aware semantic search.
2
+
3
+ ALTER TABLE public.document_chunks
4
+ ADD COLUMN IF NOT EXISTS workspace_id UUID;
5
+
6
+ UPDATE public.document_chunks dc
7
+ SET workspace_id = i.workspace_id
8
+ FROM public.ingestions i
9
+ WHERE dc.workspace_id IS NULL
10
+ AND dc.ingestion_id = i.id;
11
+
12
+ ALTER TABLE public.document_chunks
13
+ ALTER COLUMN workspace_id SET NOT NULL;
14
+
15
+ DO $$
16
+ BEGIN
17
+ IF NOT EXISTS (
18
+ SELECT 1 FROM pg_constraint
19
+ WHERE conname = 'document_chunks_workspace_id_fkey'
20
+ ) THEN
21
+ ALTER TABLE public.document_chunks
22
+ ADD CONSTRAINT document_chunks_workspace_id_fkey
23
+ FOREIGN KEY (workspace_id) REFERENCES public.workspaces(id) ON DELETE CASCADE;
24
+ END IF;
25
+ END $$;
26
+
27
+ CREATE INDEX IF NOT EXISTS document_chunks_workspace_id_idx
28
+ ON public.document_chunks(workspace_id);
29
+
30
+ CREATE INDEX IF NOT EXISTS document_chunks_workspace_model_scope_idx
31
+ ON public.document_chunks(workspace_id, embedding_provider, embedding_model, vector_dim);
32
+
33
+ DROP POLICY IF EXISTS "Users can manage their own document chunks" ON public.document_chunks;
34
+ DROP POLICY IF EXISTS "Workspace members can read document chunks" ON public.document_chunks;
35
+ DROP POLICY IF EXISTS "Workspace members can insert document chunks" ON public.document_chunks;
36
+ DROP POLICY IF EXISTS "Workspace members can update document chunks" ON public.document_chunks;
37
+ DROP POLICY IF EXISTS "Workspace members can delete document chunks" ON public.document_chunks;
38
+
39
+ CREATE POLICY "Workspace members can read document chunks"
40
+ ON public.document_chunks FOR SELECT
41
+ USING (public.is_workspace_member(workspace_id));
42
+
43
+ CREATE POLICY "Workspace members can insert document chunks"
44
+ ON public.document_chunks FOR INSERT
45
+ WITH CHECK (public.is_workspace_member(workspace_id) AND user_id = auth.uid());
46
+
47
+ CREATE POLICY "Workspace members can update document chunks"
48
+ ON public.document_chunks FOR UPDATE
49
+ USING (public.is_workspace_member(workspace_id))
50
+ WITH CHECK (public.is_workspace_member(workspace_id));
51
+
52
+ CREATE POLICY "Workspace members can delete document chunks"
53
+ ON public.document_chunks FOR DELETE
54
+ USING (public.is_workspace_member(workspace_id));
55
+
56
+ CREATE OR REPLACE FUNCTION public.search_workspace_documents(
57
+ p_workspace_id UUID,
58
+ p_embedding_provider TEXT,
59
+ p_embedding_model TEXT,
60
+ query_embedding vector,
61
+ match_threshold float DEFAULT 0.7,
62
+ match_count int DEFAULT 5,
63
+ query_dim int DEFAULT 1536
64
+ )
65
+ RETURNS TABLE (
66
+ id UUID,
67
+ ingestion_id UUID,
68
+ content TEXT,
69
+ similarity float
70
+ )
71
+ LANGUAGE plpgsql
72
+ AS $$
73
+ BEGIN
74
+ IF query_dim = 384 THEN
75
+ RETURN QUERY
76
+ SELECT
77
+ dc.id,
78
+ dc.ingestion_id,
79
+ dc.content,
80
+ 1 - (dc.embedding::vector(384) <=> query_embedding::vector(384)) AS similarity
81
+ FROM public.document_chunks dc
82
+ WHERE dc.workspace_id = p_workspace_id
83
+ AND dc.embedding_provider = p_embedding_provider
84
+ AND dc.embedding_model = p_embedding_model
85
+ AND dc.vector_dim = 384
86
+ AND 1 - (dc.embedding::vector(384) <=> query_embedding::vector(384)) > match_threshold
87
+ ORDER BY dc.embedding::vector(384) <=> query_embedding::vector(384)
88
+ LIMIT match_count;
89
+ ELSIF query_dim = 768 THEN
90
+ RETURN QUERY
91
+ SELECT
92
+ dc.id,
93
+ dc.ingestion_id,
94
+ dc.content,
95
+ 1 - (dc.embedding::vector(768) <=> query_embedding::vector(768)) AS similarity
96
+ FROM public.document_chunks dc
97
+ WHERE dc.workspace_id = p_workspace_id
98
+ AND dc.embedding_provider = p_embedding_provider
99
+ AND dc.embedding_model = p_embedding_model
100
+ AND dc.vector_dim = 768
101
+ AND 1 - (dc.embedding::vector(768) <=> query_embedding::vector(768)) > match_threshold
102
+ ORDER BY dc.embedding::vector(768) <=> query_embedding::vector(768)
103
+ LIMIT match_count;
104
+ ELSIF query_dim = 1536 THEN
105
+ RETURN QUERY
106
+ SELECT
107
+ dc.id,
108
+ dc.ingestion_id,
109
+ dc.content,
110
+ 1 - (dc.embedding::vector(1536) <=> query_embedding::vector(1536)) AS similarity
111
+ FROM public.document_chunks dc
112
+ WHERE dc.workspace_id = p_workspace_id
113
+ AND dc.embedding_provider = p_embedding_provider
114
+ AND dc.embedding_model = p_embedding_model
115
+ AND dc.vector_dim = 1536
116
+ AND 1 - (dc.embedding::vector(1536) <=> query_embedding::vector(1536)) > match_threshold
117
+ ORDER BY dc.embedding::vector(1536) <=> query_embedding::vector(1536)
118
+ LIMIT match_count;
119
+ ELSE
120
+ RETURN QUERY
121
+ SELECT
122
+ dc.id,
123
+ dc.ingestion_id,
124
+ dc.content,
125
+ 1 - (dc.embedding <=> query_embedding) AS similarity
126
+ FROM public.document_chunks dc
127
+ WHERE dc.workspace_id = p_workspace_id
128
+ AND dc.embedding_provider = p_embedding_provider
129
+ AND dc.embedding_model = p_embedding_model
130
+ AND dc.vector_dim = query_dim
131
+ AND 1 - (dc.embedding <=> query_embedding) > match_threshold
132
+ ORDER BY dc.embedding <=> query_embedding
133
+ LIMIT match_count;
134
+ END IF;
135
+ END;
136
+ $$;
137
+
138
+ COMMENT ON FUNCTION public.search_workspace_documents
139
+ IS 'Performs cosine similarity search against document chunks scoped to a workspace.';