@realtimex/folio 0.1.15 → 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 (50) 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 +51 -6
  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/api/src/services/SDKService.ts +48 -2
  15. package/dist/api/src/middleware/auth.js +59 -0
  16. package/dist/api/src/routes/chat.js +1 -1
  17. package/dist/api/src/routes/index.js +2 -0
  18. package/dist/api/src/routes/ingestions.js +51 -9
  19. package/dist/api/src/routes/policies.js +49 -7
  20. package/dist/api/src/routes/stats.js +9 -5
  21. package/dist/api/src/routes/workspaces.js +220 -0
  22. package/dist/api/src/services/ChatService.js +7 -2
  23. package/dist/api/src/services/IngestionService.js +35 -30
  24. package/dist/api/src/services/PolicyEngine.js +2 -1
  25. package/dist/api/src/services/PolicyLearningService.js +28 -6
  26. package/dist/api/src/services/PolicyLoader.js +29 -25
  27. package/dist/api/src/services/RAGService.js +43 -11
  28. package/dist/api/src/services/SDKService.js +37 -2
  29. package/dist/assets/index-CTn5FcC4.js +113 -0
  30. package/dist/assets/index-Dq9sxoZK.css +1 -0
  31. package/dist/index.html +2 -2
  32. package/docs-dev/ingestion-engine.md +3 -3
  33. package/package.json +1 -1
  34. package/supabase/functions/workspace-invite/index.ts +110 -0
  35. package/supabase/migrations/20260223000000_initial_foundation.sql +5 -0
  36. package/supabase/migrations/20260224000004_add_avatars_storage.sql +4 -0
  37. package/supabase/migrations/20260224000006_add_policies_table.sql +5 -0
  38. package/supabase/migrations/20260224000008_add_ingestions_table.sql +2 -0
  39. package/supabase/migrations/20260225000000_setup_compatible_mode.sql +17 -4
  40. package/supabase/migrations/20260225000003_add_baseline_configs.sql +4 -3
  41. package/supabase/migrations/20260226000000_add_processing_events.sql +1 -0
  42. package/supabase/migrations/20260226000002_add_dynamic_rag.sql +1 -0
  43. package/supabase/migrations/20260226000005_add_chat_tables.sql +3 -0
  44. package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +4 -0
  45. package/supabase/migrations/20260302064608_add_ingestion_llm_settings_compat.sql +15 -0
  46. package/supabase/migrations/20260303000000_add_workspaces_phase1.sql +459 -0
  47. package/supabase/migrations/20260303010000_add_workspace_management_rpc.sql +310 -0
  48. package/supabase/migrations/20260303020000_workspace_scope_document_chunks.sql +139 -0
  49. package/dist/assets/index-Cj989Mcp.js +0 -113
  50. package/dist/assets/index-DzN8-j-e.css +0 -1
@@ -7,6 +7,7 @@ import crypto from "crypto";
7
7
  import { asyncHandler } from "../middleware/errorHandler.js";
8
8
  import { optionalAuth } from "../middleware/auth.js";
9
9
  import { IngestionService } from "../services/IngestionService.js";
10
+ import { SDKService } from "../services/SDKService.js";
10
11
  const router = Router();
11
12
  const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
12
13
  router.use(optionalAuth);
@@ -16,10 +17,14 @@ router.get("/", asyncHandler(async (req, res) => {
16
17
  res.status(401).json({ success: false, error: "Authentication required" });
17
18
  return;
18
19
  }
20
+ if (!req.workspaceId) {
21
+ res.status(403).json({ success: false, error: "Workspace membership required" });
22
+ return;
23
+ }
19
24
  const page = Math.max(1, parseInt(req.query["page"]) || 1);
20
25
  const pageSize = Math.min(100, Math.max(1, parseInt(req.query["pageSize"]) || 20));
21
26
  const query = req.query["q"]?.trim() || undefined;
22
- const { ingestions, total } = await IngestionService.list(req.supabase, req.user.id, { page, pageSize, query });
27
+ const { ingestions, total } = await IngestionService.list(req.supabase, req.workspaceId, { page, pageSize, query });
23
28
  res.json({ success: true, ingestions, total, page, pageSize });
24
29
  }));
25
30
  // GET /api/ingestions/:id — get single ingestion
@@ -28,7 +33,11 @@ router.get("/:id", asyncHandler(async (req, res) => {
28
33
  res.status(401).json({ success: false, error: "Authentication required" });
29
34
  return;
30
35
  }
31
- const ingestion = await IngestionService.get(req.params["id"], req.supabase, req.user.id);
36
+ if (!req.workspaceId) {
37
+ res.status(403).json({ success: false, error: "Workspace membership required" });
38
+ return;
39
+ }
40
+ const ingestion = await IngestionService.get(req.params["id"], req.supabase, req.workspaceId);
32
41
  if (!ingestion) {
33
42
  res.status(404).json({ success: false, error: "Not found" });
34
43
  return;
@@ -41,6 +50,10 @@ router.post("/upload", upload.single("file"), asyncHandler(async (req, res) => {
41
50
  res.status(401).json({ success: false, error: "Authentication required" });
42
51
  return;
43
52
  }
53
+ if (!req.workspaceId) {
54
+ res.status(403).json({ success: false, error: "Workspace membership required" });
55
+ return;
56
+ }
44
57
  const file = req.file;
45
58
  if (!file) {
46
59
  res.status(400).json({ success: false, error: "No file uploaded" });
@@ -52,7 +65,11 @@ router.post("/upload", upload.single("file"), asyncHandler(async (req, res) => {
52
65
  .select("storage_path")
53
66
  .eq("user_id", req.user.id)
54
67
  .maybeSingle();
55
- const dropzoneDir = settings?.storage_path || path.join(os.homedir(), ".realtimex", "folio", "dropzone");
68
+ const configuredStoragePath = typeof settings?.storage_path === "string" ? settings.storage_path.trim() : "";
69
+ const legacyDefaultDropzoneDir = path.join(os.homedir(), ".realtimex", "folio", "dropzone");
70
+ const dropzoneDir = !configuredStoragePath || configuredStoragePath === legacyDefaultDropzoneDir
71
+ ? await SDKService.getDefaultDropzoneDir()
72
+ : configuredStoragePath;
56
73
  await fs.mkdir(dropzoneDir, { recursive: true });
57
74
  // Compute SHA-256 hash before writing — used for deduplication
58
75
  const fileHash = crypto.createHash("sha256").update(file.buffer).digest("hex");
@@ -65,6 +82,7 @@ router.post("/upload", upload.single("file"), asyncHandler(async (req, res) => {
65
82
  const ingestion = await IngestionService.ingest({
66
83
  supabase: req.supabase,
67
84
  userId: req.user.id,
85
+ workspaceId: req.workspaceId,
68
86
  filename: file.originalname,
69
87
  mimeType: file.mimetype,
70
88
  fileSize: file.size,
@@ -81,7 +99,11 @@ router.post("/:id/rerun", asyncHandler(async (req, res) => {
81
99
  res.status(401).json({ success: false, error: "Authentication required" });
82
100
  return;
83
101
  }
84
- const matched = await IngestionService.rerun(req.params["id"], req.supabase, req.user.id);
102
+ if (!req.workspaceId) {
103
+ res.status(403).json({ success: false, error: "Workspace membership required" });
104
+ return;
105
+ }
106
+ const matched = await IngestionService.rerun(req.params["id"], req.supabase, req.user.id, req.workspaceId);
85
107
  res.json({ success: true, matched });
86
108
  }));
87
109
  // POST /api/ingestions/:id/match — manually assign a policy and optionally learn from it
@@ -90,6 +112,10 @@ router.post("/:id/match", asyncHandler(async (req, res) => {
90
112
  res.status(401).json({ success: false, error: "Authentication required" });
91
113
  return;
92
114
  }
115
+ if (!req.workspaceId) {
116
+ res.status(403).json({ success: false, error: "Workspace membership required" });
117
+ return;
118
+ }
93
119
  const policyId = typeof req.body?.policy_id === "string" ? req.body.policy_id.trim() : "";
94
120
  if (!policyId) {
95
121
  res.status(400).json({ success: false, error: "policy_id is required" });
@@ -99,7 +125,7 @@ router.post("/:id/match", asyncHandler(async (req, res) => {
99
125
  const rerun = req.body?.rerun !== false;
100
126
  const allowSideEffects = req.body?.allow_side_effects === true;
101
127
  try {
102
- const ingestion = await IngestionService.matchToPolicy(req.params["id"], policyId, req.supabase, req.user.id, {
128
+ const ingestion = await IngestionService.matchToPolicy(req.params["id"], policyId, req.supabase, req.user.id, req.workspaceId, {
103
129
  learn,
104
130
  rerun,
105
131
  allowSideEffects,
@@ -125,13 +151,17 @@ router.post("/:id/refine-policy", asyncHandler(async (req, res) => {
125
151
  res.status(401).json({ success: false, error: "Authentication required" });
126
152
  return;
127
153
  }
154
+ if (!req.workspaceId) {
155
+ res.status(403).json({ success: false, error: "Workspace membership required" });
156
+ return;
157
+ }
128
158
  const policyId = typeof req.body?.policy_id === "string" ? req.body.policy_id.trim() : "";
129
159
  if (!policyId) {
130
160
  res.status(400).json({ success: false, error: "policy_id is required" });
131
161
  return;
132
162
  }
133
163
  try {
134
- const suggestion = await IngestionService.suggestPolicyRefinement(req.params["id"], policyId, req.supabase, req.user.id, {
164
+ const suggestion = await IngestionService.suggestPolicyRefinement(req.params["id"], policyId, req.supabase, req.user.id, req.workspaceId, {
135
165
  provider: typeof req.body?.provider === "string" ? req.body.provider : undefined,
136
166
  model: typeof req.body?.model === "string" ? req.body.model : undefined,
137
167
  });
@@ -156,13 +186,17 @@ router.post("/:id/summarize", asyncHandler(async (req, res) => {
156
186
  res.status(401).json({ success: false, error: "Authentication required" });
157
187
  return;
158
188
  }
189
+ if (!req.workspaceId) {
190
+ res.status(403).json({ success: false, error: "Workspace membership required" });
191
+ return;
192
+ }
159
193
  const { data: settingsRow } = await req.supabase
160
194
  .from("user_settings")
161
195
  .select("llm_provider, llm_model, ingestion_llm_provider, ingestion_llm_model")
162
196
  .eq("user_id", req.user.id)
163
197
  .maybeSingle();
164
198
  const llmSettings = IngestionService.resolveIngestionLlmSettings(settingsRow);
165
- const summary = await IngestionService.summarize(req.params["id"], req.supabase, req.user.id, llmSettings);
199
+ const summary = await IngestionService.summarize(req.params["id"], req.supabase, req.user.id, req.workspaceId, llmSettings);
166
200
  res.json({ success: true, summary });
167
201
  }));
168
202
  // PATCH /api/ingestions/:id/tags — replace tags array (human edits)
@@ -171,6 +205,10 @@ router.patch("/:id/tags", asyncHandler(async (req, res) => {
171
205
  res.status(401).json({ success: false, error: "Authentication required" });
172
206
  return;
173
207
  }
208
+ if (!req.workspaceId) {
209
+ res.status(403).json({ success: false, error: "Workspace membership required" });
210
+ return;
211
+ }
174
212
  const tags = req.body?.tags;
175
213
  if (!Array.isArray(tags) || tags.some((t) => typeof t !== "string")) {
176
214
  res.status(400).json({ success: false, error: "tags must be an array of strings" });
@@ -181,7 +219,7 @@ router.patch("/:id/tags", asyncHandler(async (req, res) => {
181
219
  .from("ingestions")
182
220
  .update({ tags: normalized })
183
221
  .eq("id", req.params["id"])
184
- .eq("user_id", req.user.id);
222
+ .eq("workspace_id", req.workspaceId);
185
223
  if (error) {
186
224
  res.status(500).json({ success: false, error: error.message });
187
225
  return;
@@ -194,7 +232,11 @@ router.delete("/:id", asyncHandler(async (req, res) => {
194
232
  res.status(401).json({ success: false, error: "Authentication required" });
195
233
  return;
196
234
  }
197
- const deleted = await IngestionService.delete(req.params["id"], req.supabase, req.user.id);
235
+ if (!req.workspaceId) {
236
+ res.status(403).json({ success: false, error: "Workspace membership required" });
237
+ return;
238
+ }
239
+ const deleted = await IngestionService.delete(req.params["id"], req.supabase, req.workspaceId);
198
240
  if (!deleted) {
199
241
  res.status(404).json({ success: false, error: "Not found" });
200
242
  return;
@@ -10,14 +10,24 @@ const router = Router();
10
10
  router.use(optionalAuth);
11
11
  // GET /api/policies — list all loaded policies
12
12
  router.get("/", asyncHandler(async (req, res) => {
13
- const policies = await PolicyLoader.load(false, req.supabase);
14
- if (!req.supabase || !req.user || policies.length === 0) {
13
+ if (!req.supabase || !req.user) {
14
+ const policies = await PolicyLoader.load(false, req.supabase, req.workspaceId);
15
+ res.json({ success: true, policies });
16
+ return;
17
+ }
18
+ if (!req.workspaceId) {
19
+ res.status(403).json({ success: false, error: "Workspace membership required" });
20
+ return;
21
+ }
22
+ const policies = await PolicyLoader.load(false, req.supabase, req.workspaceId);
23
+ if (policies.length === 0) {
15
24
  res.json({ success: true, policies });
16
25
  return;
17
26
  }
18
27
  const stats = await PolicyLearningService.getPolicyLearningStats({
19
28
  supabase: req.supabase,
20
29
  userId: req.user.id,
30
+ workspaceId: req.workspaceId,
21
31
  policyIds: policies.map((policy) => policy.metadata.id),
22
32
  });
23
33
  const enrichedPolicies = policies.map((policy) => {
@@ -35,17 +45,33 @@ router.get("/", asyncHandler(async (req, res) => {
35
45
  }));
36
46
  // POST /api/policies — save a new policy
37
47
  router.post("/", asyncHandler(async (req, res) => {
48
+ if (!req.supabase || !req.user) {
49
+ res.status(401).json({ success: false, error: "Authentication required" });
50
+ return;
51
+ }
52
+ if (!req.workspaceId) {
53
+ res.status(403).json({ success: false, error: "Workspace membership required" });
54
+ return;
55
+ }
38
56
  const policy = req.body;
39
57
  if (!PolicyLoader.validate(policy)) {
40
58
  res.status(400).json({ success: false, error: "Invalid policy schema" });
41
59
  return;
42
60
  }
43
- const filePath = await PolicyLoader.save(policy, req.supabase, req.user?.id);
61
+ const filePath = await PolicyLoader.save(policy, req.supabase, req.user.id, req.workspaceId);
44
62
  res.status(201).json({ success: true, filePath });
45
63
  }));
46
64
  // DELETE /api/policies/:id — delete a policy by ID
47
65
  router.delete("/:id", asyncHandler(async (req, res) => {
48
- const deleted = await PolicyLoader.delete(req.params["id"], req.supabase, req.user?.id);
66
+ if (!req.supabase || !req.user) {
67
+ res.status(401).json({ success: false, error: "Authentication required" });
68
+ return;
69
+ }
70
+ if (!req.workspaceId) {
71
+ res.status(403).json({ success: false, error: "Workspace membership required" });
72
+ return;
73
+ }
74
+ const deleted = await PolicyLoader.delete(req.params["id"], req.supabase, req.user.id, req.workspaceId);
49
75
  if (!deleted) {
50
76
  res.status(404).json({ success: false, error: "Policy not found" });
51
77
  return;
@@ -54,14 +80,30 @@ router.delete("/:id", asyncHandler(async (req, res) => {
54
80
  }));
55
81
  // PATCH /api/policies/:id — partial update (enabled toggle, metadata fields)
56
82
  router.patch("/:id", asyncHandler(async (req, res) => {
83
+ if (!req.supabase || !req.user) {
84
+ res.status(401).json({ success: false, error: "Authentication required" });
85
+ return;
86
+ }
87
+ if (!req.workspaceId) {
88
+ res.status(403).json({ success: false, error: "Workspace membership required" });
89
+ return;
90
+ }
57
91
  const { enabled, name, description, tags, priority } = req.body;
58
- await PolicyLoader.patch(req.params["id"], { enabled, name, description, tags, priority }, req.supabase, req.user?.id);
92
+ await PolicyLoader.patch(req.params["id"], { enabled, name, description, tags, priority }, req.supabase, req.user.id, req.workspaceId);
59
93
  res.json({ success: true });
60
94
  }));
61
95
  // POST /api/policies/reload — force cache invalidation
62
96
  router.post("/reload", asyncHandler(async (req, res) => {
63
- PolicyLoader.invalidateCache();
64
- const policies = await PolicyLoader.load(true, req.supabase);
97
+ if (!req.supabase || !req.user) {
98
+ res.status(401).json({ success: false, error: "Authentication required" });
99
+ return;
100
+ }
101
+ if (!req.workspaceId) {
102
+ res.status(403).json({ success: false, error: "Workspace membership required" });
103
+ return;
104
+ }
105
+ PolicyLoader.invalidateCache(req.workspaceId);
106
+ const policies = await PolicyLoader.load(true, req.supabase, req.workspaceId);
65
107
  res.json({ success: true, count: policies.length });
66
108
  }));
67
109
  // POST /api/policies/synthesize — NL → Policy via LLM
@@ -9,29 +9,33 @@ router.get("/", async (req, res) => {
9
9
  return;
10
10
  }
11
11
  try {
12
- const userId = req.user.id;
12
+ const workspaceId = req.workspaceId;
13
+ if (!workspaceId) {
14
+ res.status(403).json({ success: false, error: "Workspace membership required" });
15
+ return;
16
+ }
13
17
  const s = req.supabase; // the scoped service client
14
18
  // 1. Total Documents Ingested
15
19
  const { count: totalDocumentsCount, error: err1 } = await s
16
20
  .from("ingestions")
17
21
  .select("*", { count: 'exact', head: true })
18
- .eq("user_id", userId);
22
+ .eq("workspace_id", workspaceId);
19
23
  // 2. Active Policies
20
24
  const { count: activePoliciesCount, error: err2 } = await s
21
25
  .from("policies")
22
26
  .select("*", { count: 'exact', head: true })
23
- .eq("user_id", userId)
27
+ .eq("workspace_id", workspaceId)
24
28
  .eq("enabled", true);
25
29
  // 3. RAG Knowledge Base (Chunks)
26
30
  const { count: ragChunksCount, error: err3 } = await s
27
31
  .from("document_chunks")
28
32
  .select("*", { count: 'exact', head: true })
29
- .eq("user_id", userId);
33
+ .eq("workspace_id", workspaceId);
30
34
  // 4. Automation Runs (Sum of actions taken across ingestions)
31
35
  const { data: ingestionsWithActions, error: err4 } = await s
32
36
  .from("ingestions")
33
37
  .select("actions_taken")
34
- .eq("user_id", userId)
38
+ .eq("workspace_id", workspaceId)
35
39
  .not("actions_taken", "is", null);
36
40
  let automationRunsCount = 0;
37
41
  if (ingestionsWithActions) {
@@ -0,0 +1,220 @@
1
+ import { Router } from "express";
2
+ import { config } from "../config/index.js";
3
+ import { optionalAuth } from "../middleware/auth.js";
4
+ import { asyncHandler } from "../middleware/errorHandler.js";
5
+ import { getSupabaseConfigFromHeaders } from "../services/supabase.js";
6
+ function mapRpcStatus(errorCode) {
7
+ if (errorCode === "42501")
8
+ return 403;
9
+ if (errorCode === "22023")
10
+ return 400;
11
+ return 500;
12
+ }
13
+ const router = Router();
14
+ router.use(optionalAuth);
15
+ router.get("/", asyncHandler(async (req, res) => {
16
+ if (!req.user || !req.supabase) {
17
+ res.status(401).json({ success: false, error: "Authentication required" });
18
+ return;
19
+ }
20
+ const { data: membershipsData, error: membershipsError } = await req.supabase
21
+ .from("workspace_members")
22
+ .select("workspace_id,role,status,created_at,updated_at")
23
+ .eq("user_id", req.user.id)
24
+ .eq("status", "active")
25
+ .order("created_at", { ascending: true });
26
+ if (membershipsError) {
27
+ const code = membershipsError.code;
28
+ // Backward compatibility for projects before workspace migration.
29
+ if (code === "42P01") {
30
+ res.json({
31
+ success: true,
32
+ workspaces: [],
33
+ activeWorkspaceId: null,
34
+ activeWorkspaceRole: null,
35
+ });
36
+ return;
37
+ }
38
+ res.status(500).json({ success: false, error: membershipsError.message });
39
+ return;
40
+ }
41
+ const memberships = (membershipsData ?? []);
42
+ if (memberships.length === 0) {
43
+ res.json({
44
+ success: true,
45
+ workspaces: [],
46
+ activeWorkspaceId: null,
47
+ activeWorkspaceRole: null,
48
+ });
49
+ return;
50
+ }
51
+ const workspaceIds = memberships.map((membership) => membership.workspace_id);
52
+ const { data: workspaceData, error: workspaceError } = await req.supabase
53
+ .from("workspaces")
54
+ .select("id,name,owner_user_id,created_at,updated_at")
55
+ .in("id", workspaceIds);
56
+ if (workspaceError) {
57
+ res.status(500).json({ success: false, error: workspaceError.message });
58
+ return;
59
+ }
60
+ const workspaceMap = new Map();
61
+ for (const workspace of (workspaceData ?? [])) {
62
+ workspaceMap.set(workspace.id, workspace);
63
+ }
64
+ const workspaces = memberships
65
+ .map((membership) => {
66
+ const workspace = workspaceMap.get(membership.workspace_id);
67
+ if (!workspace)
68
+ return null;
69
+ return {
70
+ id: workspace.id,
71
+ name: workspace.name,
72
+ owner_user_id: workspace.owner_user_id,
73
+ created_at: workspace.created_at,
74
+ updated_at: workspace.updated_at,
75
+ role: membership.role,
76
+ membership_status: membership.status,
77
+ membership_created_at: membership.created_at,
78
+ };
79
+ })
80
+ .filter((workspace) => Boolean(workspace));
81
+ const activeWorkspaceId = req.workspaceId ?? workspaces[0]?.id ?? null;
82
+ const activeWorkspace = workspaces.find((workspace) => workspace.id === activeWorkspaceId) ?? null;
83
+ res.json({
84
+ success: true,
85
+ workspaces,
86
+ activeWorkspaceId,
87
+ activeWorkspaceRole: activeWorkspace?.role ?? null,
88
+ });
89
+ }));
90
+ router.get("/:workspaceId/members", asyncHandler(async (req, res) => {
91
+ if (!req.user || !req.supabase) {
92
+ res.status(401).json({ success: false, error: "Authentication required" });
93
+ return;
94
+ }
95
+ const workspaceId = req.params["workspaceId"];
96
+ if (!workspaceId) {
97
+ res.status(400).json({ success: false, error: "workspaceId is required" });
98
+ return;
99
+ }
100
+ const { data, error } = await req.supabase.rpc("workspace_list_members", {
101
+ p_workspace_id: workspaceId,
102
+ });
103
+ if (error) {
104
+ const status = mapRpcStatus(error.code);
105
+ res.status(status).json({ success: false, error: error.message });
106
+ return;
107
+ }
108
+ res.json({
109
+ success: true,
110
+ members: (data ?? []),
111
+ });
112
+ }));
113
+ router.post("/:workspaceId/members/invite", asyncHandler(async (req, res) => {
114
+ if (!req.user || !req.supabase) {
115
+ res.status(401).json({ success: false, error: "Authentication required" });
116
+ return;
117
+ }
118
+ const workspaceId = req.params["workspaceId"];
119
+ const email = typeof req.body?.email === "string" ? req.body.email.trim() : "";
120
+ const role = req.body?.role === "admin" ? "admin" : "member";
121
+ if (!workspaceId) {
122
+ res.status(400).json({ success: false, error: "workspaceId is required" });
123
+ return;
124
+ }
125
+ if (!email) {
126
+ res.status(400).json({ success: false, error: "email is required" });
127
+ return;
128
+ }
129
+ const headerConfig = getSupabaseConfigFromHeaders(req.headers);
130
+ const envUrl = config.supabase.url;
131
+ const envKey = config.supabase.anonKey;
132
+ const envIsValid = envUrl.startsWith("http://") || envUrl.startsWith("https://");
133
+ const supabaseUrl = envIsValid && envKey ? envUrl : headerConfig?.url;
134
+ const anonKey = envIsValid && envKey ? envKey : headerConfig?.anonKey;
135
+ if (!supabaseUrl || !anonKey) {
136
+ res.status(500).json({ success: false, error: "Supabase config unavailable for invite workflow." });
137
+ return;
138
+ }
139
+ const authHeader = req.headers.authorization;
140
+ if (!authHeader?.startsWith("Bearer ")) {
141
+ res.status(401).json({ success: false, error: "Authentication required" });
142
+ return;
143
+ }
144
+ const response = await fetch(`${supabaseUrl}/functions/v1/workspace-invite`, {
145
+ method: "POST",
146
+ headers: {
147
+ "Content-Type": "application/json",
148
+ Authorization: authHeader,
149
+ apikey: anonKey,
150
+ },
151
+ body: JSON.stringify({
152
+ workspace_id: workspaceId,
153
+ email,
154
+ role,
155
+ }),
156
+ });
157
+ const payload = await response.json().catch(() => ({}));
158
+ if (!response.ok) {
159
+ res.status(response.status).json({
160
+ success: false,
161
+ error: payload?.error?.message || payload?.error || `Invite workflow failed (${response.status})`,
162
+ });
163
+ return;
164
+ }
165
+ res.json(payload);
166
+ }));
167
+ router.patch("/:workspaceId/members/:userId", asyncHandler(async (req, res) => {
168
+ if (!req.user || !req.supabase) {
169
+ res.status(401).json({ success: false, error: "Authentication required" });
170
+ return;
171
+ }
172
+ const workspaceId = req.params["workspaceId"];
173
+ const userId = req.params["userId"];
174
+ const role = req.body?.role === "admin" ? "admin" : req.body?.role === "member" ? "member" : "";
175
+ if (!workspaceId || !userId) {
176
+ res.status(400).json({ success: false, error: "workspaceId and userId are required" });
177
+ return;
178
+ }
179
+ if (!role) {
180
+ res.status(400).json({ success: false, error: "role must be admin or member" });
181
+ return;
182
+ }
183
+ const { data, error } = await req.supabase.rpc("workspace_update_member_role", {
184
+ p_workspace_id: workspaceId,
185
+ p_target_user_id: userId,
186
+ p_role: role,
187
+ });
188
+ if (error) {
189
+ const status = mapRpcStatus(error.code);
190
+ res.status(status).json({ success: false, error: error.message });
191
+ return;
192
+ }
193
+ res.json({
194
+ success: true,
195
+ member: Array.isArray(data) ? data[0] ?? null : null,
196
+ });
197
+ }));
198
+ router.delete("/:workspaceId/members/:userId", asyncHandler(async (req, res) => {
199
+ if (!req.user || !req.supabase) {
200
+ res.status(401).json({ success: false, error: "Authentication required" });
201
+ return;
202
+ }
203
+ const workspaceId = req.params["workspaceId"];
204
+ const userId = req.params["userId"];
205
+ if (!workspaceId || !userId) {
206
+ res.status(400).json({ success: false, error: "workspaceId and userId are required" });
207
+ return;
208
+ }
209
+ const { data, error } = await req.supabase.rpc("workspace_remove_member", {
210
+ p_workspace_id: workspaceId,
211
+ p_target_user_id: userId,
212
+ });
213
+ if (error) {
214
+ const status = mapRpcStatus(error.code);
215
+ res.status(status).json({ success: false, error: error.message });
216
+ return;
217
+ }
218
+ res.json({ success: true, removed: data === true });
219
+ }));
220
+ export default router;
@@ -9,7 +9,7 @@ export class ChatService {
9
9
  * Send a message to an existing session, augment with RAG context if needed,
10
10
  * and stream the AI response back into the database.
11
11
  */
12
- static async handleMessage(sessionId, userId, content, supabase) {
12
+ static async handleMessage(sessionId, userId, content, supabase, workspaceId) {
13
13
  // 1. Get User/Session Settings (Models to use)
14
14
  const { data: settings } = await supabase
15
15
  .from("user_settings")
@@ -49,7 +49,12 @@ export class ChatService {
49
49
  embedding_model: embedSettings.embedding_model ?? "auto",
50
50
  query_preview: content.slice(0, 180),
51
51
  }, supabase);
52
- contextSources = await RAGService.searchDocuments(content, userId, supabase, { topK: 5, similarityThreshold: 0.65, settings: embedSettings });
52
+ contextSources = await RAGService.searchDocuments(content, userId, supabase, {
53
+ topK: 5,
54
+ similarityThreshold: 0.65,
55
+ settings: embedSettings,
56
+ workspaceId
57
+ });
53
58
  Actuator.logEvent(null, userId, "analysis", "RAG Retrieval", {
54
59
  action: "RAG query response",
55
60
  session_id: sessionId,