@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.
- package/api/src/middleware/auth.ts +77 -0
- package/api/src/routes/chat.ts +7 -1
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/ingestions.ts +51 -6
- package/api/src/routes/policies.ts +50 -7
- package/api/src/routes/stats.ts +9 -5
- package/api/src/routes/workspaces.ts +290 -0
- package/api/src/services/ChatService.ts +8 -2
- package/api/src/services/IngestionService.ts +38 -26
- package/api/src/services/PolicyEngine.ts +4 -1
- package/api/src/services/PolicyLearningService.ts +31 -6
- package/api/src/services/PolicyLoader.ts +44 -25
- package/api/src/services/RAGService.ts +52 -12
- package/api/src/services/SDKService.ts +48 -2
- package/dist/api/src/middleware/auth.js +59 -0
- package/dist/api/src/routes/chat.js +1 -1
- package/dist/api/src/routes/index.js +2 -0
- package/dist/api/src/routes/ingestions.js +51 -9
- package/dist/api/src/routes/policies.js +49 -7
- package/dist/api/src/routes/stats.js +9 -5
- package/dist/api/src/routes/workspaces.js +220 -0
- package/dist/api/src/services/ChatService.js +7 -2
- package/dist/api/src/services/IngestionService.js +35 -30
- package/dist/api/src/services/PolicyEngine.js +2 -1
- package/dist/api/src/services/PolicyLearningService.js +28 -6
- package/dist/api/src/services/PolicyLoader.js +29 -25
- package/dist/api/src/services/RAGService.js +43 -11
- package/dist/api/src/services/SDKService.js +37 -2
- package/dist/assets/index-CTn5FcC4.js +113 -0
- package/dist/assets/index-Dq9sxoZK.css +1 -0
- package/dist/index.html +2 -2
- package/docs-dev/ingestion-engine.md +3 -3
- package/package.json +1 -1
- package/supabase/functions/workspace-invite/index.ts +110 -0
- package/supabase/migrations/20260223000000_initial_foundation.sql +5 -0
- package/supabase/migrations/20260224000004_add_avatars_storage.sql +4 -0
- package/supabase/migrations/20260224000006_add_policies_table.sql +5 -0
- package/supabase/migrations/20260224000008_add_ingestions_table.sql +2 -0
- package/supabase/migrations/20260225000000_setup_compatible_mode.sql +17 -4
- package/supabase/migrations/20260225000003_add_baseline_configs.sql +4 -3
- package/supabase/migrations/20260226000000_add_processing_events.sql +1 -0
- package/supabase/migrations/20260226000002_add_dynamic_rag.sql +1 -0
- package/supabase/migrations/20260226000005_add_chat_tables.sql +3 -0
- package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +4 -0
- package/supabase/migrations/20260302064608_add_ingestion_llm_settings_compat.sql +15 -0
- package/supabase/migrations/20260303000000_add_workspaces_phase1.sql +459 -0
- package/supabase/migrations/20260303010000_add_workspace_management_rpc.sql +310 -0
- package/supabase/migrations/20260303020000_workspace_scope_document_chunks.sql +139 -0
- package/dist/assets/index-Cj989Mcp.js +0 -113
- 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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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("
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
64
|
-
|
|
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
|
|
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("
|
|
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("
|
|
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("
|
|
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("
|
|
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, {
|
|
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,
|