@realtimex/folio 0.1.2
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/.env.example +20 -0
- package/README.md +63 -0
- package/api/server.ts +130 -0
- package/api/src/config/index.ts +96 -0
- package/api/src/middleware/auth.ts +128 -0
- package/api/src/middleware/errorHandler.ts +88 -0
- package/api/src/middleware/index.ts +4 -0
- package/api/src/middleware/rateLimit.ts +71 -0
- package/api/src/middleware/validation.ts +58 -0
- package/api/src/routes/accounts.ts +142 -0
- package/api/src/routes/baseline-config.ts +124 -0
- package/api/src/routes/chat.ts +154 -0
- package/api/src/routes/health.ts +61 -0
- package/api/src/routes/index.ts +35 -0
- package/api/src/routes/ingestions.ts +275 -0
- package/api/src/routes/migrate.ts +112 -0
- package/api/src/routes/policies.ts +121 -0
- package/api/src/routes/processing.ts +90 -0
- package/api/src/routes/rules.ts +11 -0
- package/api/src/routes/sdk.ts +100 -0
- package/api/src/routes/settings.ts +80 -0
- package/api/src/routes/setup.ts +389 -0
- package/api/src/routes/stats.ts +81 -0
- package/api/src/routes/tts.ts +190 -0
- package/api/src/services/BaselineConfigService.ts +208 -0
- package/api/src/services/ChatService.ts +204 -0
- package/api/src/services/GoogleDriveService.ts +331 -0
- package/api/src/services/GoogleSheetsService.ts +1107 -0
- package/api/src/services/IngestionService.ts +1187 -0
- package/api/src/services/ModelCapabilityService.ts +248 -0
- package/api/src/services/PolicyEngine.ts +1625 -0
- package/api/src/services/PolicyLearningService.ts +527 -0
- package/api/src/services/PolicyLoader.ts +249 -0
- package/api/src/services/RAGService.ts +391 -0
- package/api/src/services/SDKService.ts +249 -0
- package/api/src/services/supabase.ts +113 -0
- package/api/src/utils/Actuator.ts +284 -0
- package/api/src/utils/actions/ActionHandler.ts +34 -0
- package/api/src/utils/actions/AppendToGSheetAction.ts +260 -0
- package/api/src/utils/actions/AutoRenameAction.ts +58 -0
- package/api/src/utils/actions/CopyAction.ts +120 -0
- package/api/src/utils/actions/CopyToGDriveAction.ts +64 -0
- package/api/src/utils/actions/LogCsvAction.ts +48 -0
- package/api/src/utils/actions/NotifyAction.ts +39 -0
- package/api/src/utils/actions/RenameAction.ts +57 -0
- package/api/src/utils/actions/WebhookAction.ts +58 -0
- package/api/src/utils/actions/utils.ts +293 -0
- package/api/src/utils/llmResponse.ts +61 -0
- package/api/src/utils/logger.ts +67 -0
- package/bin/folio-deploy.js +12 -0
- package/bin/folio-setup.js +45 -0
- package/bin/folio.js +65 -0
- package/dist/api/server.js +106 -0
- package/dist/api/src/config/index.js +81 -0
- package/dist/api/src/middleware/auth.js +93 -0
- package/dist/api/src/middleware/errorHandler.js +73 -0
- package/dist/api/src/middleware/index.js +4 -0
- package/dist/api/src/middleware/rateLimit.js +43 -0
- package/dist/api/src/middleware/validation.js +54 -0
- package/dist/api/src/routes/accounts.js +110 -0
- package/dist/api/src/routes/baseline-config.js +91 -0
- package/dist/api/src/routes/chat.js +114 -0
- package/dist/api/src/routes/health.js +52 -0
- package/dist/api/src/routes/index.js +31 -0
- package/dist/api/src/routes/ingestions.js +207 -0
- package/dist/api/src/routes/migrate.js +91 -0
- package/dist/api/src/routes/policies.js +86 -0
- package/dist/api/src/routes/processing.js +75 -0
- package/dist/api/src/routes/rules.js +8 -0
- package/dist/api/src/routes/sdk.js +80 -0
- package/dist/api/src/routes/settings.js +68 -0
- package/dist/api/src/routes/setup.js +315 -0
- package/dist/api/src/routes/stats.js +62 -0
- package/dist/api/src/routes/tts.js +178 -0
- package/dist/api/src/services/BaselineConfigService.js +168 -0
- package/dist/api/src/services/ChatService.js +166 -0
- package/dist/api/src/services/GoogleDriveService.js +280 -0
- package/dist/api/src/services/GoogleSheetsService.js +795 -0
- package/dist/api/src/services/IngestionService.js +990 -0
- package/dist/api/src/services/ModelCapabilityService.js +179 -0
- package/dist/api/src/services/PolicyEngine.js +1353 -0
- package/dist/api/src/services/PolicyLearningService.js +397 -0
- package/dist/api/src/services/PolicyLoader.js +159 -0
- package/dist/api/src/services/RAGService.js +295 -0
- package/dist/api/src/services/SDKService.js +212 -0
- package/dist/api/src/services/supabase.js +72 -0
- package/dist/api/src/utils/Actuator.js +225 -0
- package/dist/api/src/utils/actions/ActionHandler.js +1 -0
- package/dist/api/src/utils/actions/AppendToGSheetAction.js +191 -0
- package/dist/api/src/utils/actions/AutoRenameAction.js +49 -0
- package/dist/api/src/utils/actions/CopyAction.js +112 -0
- package/dist/api/src/utils/actions/CopyToGDriveAction.js +55 -0
- package/dist/api/src/utils/actions/LogCsvAction.js +42 -0
- package/dist/api/src/utils/actions/NotifyAction.js +32 -0
- package/dist/api/src/utils/actions/RenameAction.js +51 -0
- package/dist/api/src/utils/actions/WebhookAction.js +51 -0
- package/dist/api/src/utils/actions/utils.js +237 -0
- package/dist/api/src/utils/llmResponse.js +63 -0
- package/dist/api/src/utils/logger.js +51 -0
- package/dist/assets/index-DzN8-j-e.css +1 -0
- package/dist/assets/index-Uy-ai3Dh.js +113 -0
- package/dist/favicon.svg +31 -0
- package/dist/folio-logo.svg +46 -0
- package/dist/index.html +14 -0
- package/docs-dev/FPE-spec.md +196 -0
- package/docs-dev/folio-prd.md +47 -0
- package/docs-dev/foundation-checklist.md +30 -0
- package/docs-dev/hybrid-routing-architecture.md +205 -0
- package/docs-dev/ingestion-engine.md +69 -0
- package/docs-dev/port-from-email-automator.md +32 -0
- package/docs-dev/tech-spec.md +98 -0
- package/index.html +13 -0
- package/package.json +101 -0
- package/public/favicon.svg +31 -0
- package/public/folio-logo.svg +46 -0
- package/scripts/dev-task.mjs +51 -0
- package/scripts/get-latest-migration-timestamp.mjs +34 -0
- package/scripts/migrate.sh +91 -0
- package/supabase/.temp/cli-latest +1 -0
- package/supabase/.temp/gotrue-version +1 -0
- package/supabase/.temp/pooler-url +1 -0
- package/supabase/.temp/postgres-version +1 -0
- package/supabase/.temp/project-ref +1 -0
- package/supabase/.temp/rest-version +1 -0
- package/supabase/.temp/storage-migration +1 -0
- package/supabase/.temp/storage-version +1 -0
- package/supabase/config.toml +64 -0
- package/supabase/functions/_shared/auth.ts +35 -0
- package/supabase/functions/_shared/cors.ts +12 -0
- package/supabase/functions/_shared/supabaseAdmin.ts +17 -0
- package/supabase/functions/api-v1-settings/index.ts +66 -0
- package/supabase/functions/setup/index.ts +91 -0
- package/supabase/migrations/20260223000000_initial_foundation.sql +136 -0
- package/supabase/migrations/20260223000001_add_migration_rpc.sql +10 -0
- package/supabase/migrations/20260224000002_add_init_state_view.sql +20 -0
- package/supabase/migrations/20260224000003_port_user_creation_parity.sql +139 -0
- package/supabase/migrations/20260224000004_add_avatars_storage.sql +26 -0
- package/supabase/migrations/20260224000005_add_tts_and_embed_settings.sql +24 -0
- package/supabase/migrations/20260224000006_add_policies_table.sql +48 -0
- package/supabase/migrations/20260224000007_fix_migration_rpc.sql +9 -0
- package/supabase/migrations/20260224000008_add_ingestions_table.sql +42 -0
- package/supabase/migrations/20260225000000_setup_compatible_mode.sql +119 -0
- package/supabase/migrations/20260225000001_restore_ingestions.sql +49 -0
- package/supabase/migrations/20260225000002_add_ingestion_trace.sql +2 -0
- package/supabase/migrations/20260225000003_add_baseline_configs.sql +35 -0
- package/supabase/migrations/20260226000000_add_processing_events.sql +26 -0
- package/supabase/migrations/20260226000001_add_ingestion_file_hash.sql +10 -0
- package/supabase/migrations/20260226000002_add_dynamic_rag.sql +150 -0
- package/supabase/migrations/20260226000003_add_ingestion_summary.sql +4 -0
- package/supabase/migrations/20260226000004_add_ingestion_tags.sql +7 -0
- package/supabase/migrations/20260226000005_add_chat_tables.sql +60 -0
- package/supabase/migrations/20260227000000_harden_chat_messages_rls.sql +25 -0
- package/supabase/migrations/20260228000000_add_vision_model_capabilities.sql +8 -0
- package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +51 -0
- package/supabase/migrations/29991231235959_test_migration.sql +0 -0
- package/supabase/templates/confirmation.html +76 -0
- package/supabase/templates/email-change.html +76 -0
- package/supabase/templates/invite.html +72 -0
- package/supabase/templates/magic-link.html +68 -0
- package/supabase/templates/recovery.html +82 -0
- package/tsconfig.api.json +16 -0
- package/tsconfig.json +25 -0
- package/vite.config.ts +146 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Router, Request, Response } from "express";
|
|
2
|
+
import { SDKService } from "../services/SDKService.js";
|
|
3
|
+
import { ProvidersResponse } from "@realtimex/sdk";
|
|
4
|
+
import { createLogger } from "../utils/logger.js";
|
|
5
|
+
import { extractLlmResponse, previewLlmText } from "../utils/llmResponse.js";
|
|
6
|
+
|
|
7
|
+
const router = Router();
|
|
8
|
+
const logger = createLogger("SDKRoutes");
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* GET /api/sdk/providers/chat
|
|
12
|
+
* Returns available chat providers and their models
|
|
13
|
+
*/
|
|
14
|
+
router.get("/providers/chat", async (req: Request, res: Response) => {
|
|
15
|
+
try {
|
|
16
|
+
const sdk = SDKService.getSDK();
|
|
17
|
+
if (!sdk) {
|
|
18
|
+
return res.json({ success: false, message: "SDK not available", providers: [] });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const { providers } = await SDKService.withTimeout<ProvidersResponse>(
|
|
22
|
+
sdk.llm.chatProviders(),
|
|
23
|
+
30000,
|
|
24
|
+
"Chat providers fetch timed out"
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
res.json({ success: true, providers: providers || [] });
|
|
28
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
29
|
+
} catch (error: any) {
|
|
30
|
+
res.json({ success: false, providers: [], message: error.message });
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* GET /api/sdk/providers/embed
|
|
36
|
+
* Returns available embedding providers and their models
|
|
37
|
+
*/
|
|
38
|
+
router.get("/providers/embed", async (req: Request, res: Response) => {
|
|
39
|
+
try {
|
|
40
|
+
const sdk = SDKService.getSDK();
|
|
41
|
+
if (!sdk) {
|
|
42
|
+
return res.json({ success: false, message: "SDK not available", providers: [] });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const { providers } = await SDKService.withTimeout<ProvidersResponse>(
|
|
46
|
+
sdk.llm.embedProviders(),
|
|
47
|
+
30000,
|
|
48
|
+
"Embed providers fetch timed out"
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
res.json({ success: true, providers: providers || [] });
|
|
52
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
53
|
+
} catch (error: any) {
|
|
54
|
+
res.json({ success: false, providers: [], message: error.message });
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* POST /api/sdk/test-llm
|
|
60
|
+
* Tests connection to a specific LLM provider/model
|
|
61
|
+
*/
|
|
62
|
+
router.post("/test-llm", async (req: Request, res: Response) => {
|
|
63
|
+
try {
|
|
64
|
+
const { llm_provider, llm_model } = req.body;
|
|
65
|
+
const sdk = SDKService.getSDK();
|
|
66
|
+
if (!sdk) {
|
|
67
|
+
return res.json({ success: false, message: "SDK not available" });
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const { provider, model } = await SDKService.resolveChatProvider({
|
|
71
|
+
llm_provider,
|
|
72
|
+
llm_model
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
logger.info("LLM request (sdk test-llm)", { provider, model });
|
|
76
|
+
const response = await sdk.llm.chat(
|
|
77
|
+
[{ role: "user", content: "Say OK" }],
|
|
78
|
+
{ provider, model }
|
|
79
|
+
);
|
|
80
|
+
const raw = extractLlmResponse(response);
|
|
81
|
+
logger.info("LLM response (sdk test-llm)", {
|
|
82
|
+
provider,
|
|
83
|
+
model,
|
|
84
|
+
raw_length: raw.length,
|
|
85
|
+
raw_preview: previewLlmText(raw),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
if (response.success) {
|
|
89
|
+
res.json({ success: true, message: `Connected to ${provider}/${model}` });
|
|
90
|
+
} else {
|
|
91
|
+
res.json({ success: false, message: response.error || "Failed to connect to LLM" });
|
|
92
|
+
}
|
|
93
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
94
|
+
} catch (error: any) {
|
|
95
|
+
logger.error("LLM sdk test failed", { error: error?.message ?? String(error) });
|
|
96
|
+
res.json({ success: false, message: error.message });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
export default router;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { asyncHandler } from "../middleware/errorHandler.js";
|
|
3
|
+
import { optionalAuth } from "../middleware/auth.js";
|
|
4
|
+
|
|
5
|
+
const router = Router();
|
|
6
|
+
router.use(optionalAuth);
|
|
7
|
+
|
|
8
|
+
router.get("/", asyncHandler(async (req, res) => {
|
|
9
|
+
if (!req.supabase || !req.user) {
|
|
10
|
+
res.status(401).json({ error: "Authentication required" });
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const { data, error } = await req.supabase
|
|
15
|
+
.from("user_settings")
|
|
16
|
+
.select("*")
|
|
17
|
+
.eq("user_id", req.user.id)
|
|
18
|
+
.maybeSingle();
|
|
19
|
+
|
|
20
|
+
if (error) {
|
|
21
|
+
res.status(500).json({ error: error.message });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
res.json({ settings: data });
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
router.patch("/", asyncHandler(async (req, res) => {
|
|
29
|
+
if (!req.supabase || !req.user) {
|
|
30
|
+
res.status(401).json({ error: "Authentication required" });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const body = req.body;
|
|
35
|
+
const rawVisionMap = body.vision_model_capabilities;
|
|
36
|
+
const payload = {
|
|
37
|
+
llm_provider: body.llm_provider,
|
|
38
|
+
llm_model: body.llm_model,
|
|
39
|
+
sync_interval_minutes: body.sync_interval_minutes,
|
|
40
|
+
tts_auto_play: body.tts_auto_play,
|
|
41
|
+
tts_provider: body.tts_provider,
|
|
42
|
+
tts_voice: body.tts_voice,
|
|
43
|
+
tts_speed: body.tts_speed,
|
|
44
|
+
tts_quality: body.tts_quality,
|
|
45
|
+
embedding_provider: body.embedding_provider,
|
|
46
|
+
embedding_model: body.embedding_model,
|
|
47
|
+
storage_path: body.storage_path,
|
|
48
|
+
vision_model_capabilities: rawVisionMap && typeof rawVisionMap === "object" && !Array.isArray(rawVisionMap)
|
|
49
|
+
? rawVisionMap
|
|
50
|
+
: undefined,
|
|
51
|
+
google_client_id: body.google_client_id,
|
|
52
|
+
google_client_secret: body.google_client_secret,
|
|
53
|
+
microsoft_client_id: body.microsoft_client_id,
|
|
54
|
+
microsoft_tenant_id: body.microsoft_tenant_id
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Remove undefined fields
|
|
58
|
+
Object.keys(payload).forEach(key => {
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
60
|
+
if ((payload as any)[key] === undefined) {
|
|
61
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
62
|
+
delete (payload as any)[key];
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const { data, error } = await req.supabase
|
|
67
|
+
.from("user_settings")
|
|
68
|
+
.upsert({ user_id: req.user.id, ...payload }, { onConflict: "user_id" })
|
|
69
|
+
.select("*")
|
|
70
|
+
.single();
|
|
71
|
+
|
|
72
|
+
if (error) {
|
|
73
|
+
res.status(500).json({ error: error.message });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
res.json({ settings: data });
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
export default router;
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import axios from "axios";
|
|
4
|
+
import { createClient } from "@supabase/supabase-js";
|
|
5
|
+
import { Router } from "express";
|
|
6
|
+
|
|
7
|
+
import { asyncHandler } from "../middleware/errorHandler.js";
|
|
8
|
+
import { createLogger } from "../utils/logger.js";
|
|
9
|
+
import { schemas, validateBody } from "../middleware/validation.js";
|
|
10
|
+
|
|
11
|
+
const router = Router();
|
|
12
|
+
const logger = createLogger("SetupRoutes");
|
|
13
|
+
|
|
14
|
+
function sleep(ms: number) {
|
|
15
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function extractAxiosError(error: unknown, fallback: string): { status: number; message: string } {
|
|
19
|
+
if (!axios.isAxiosError(error)) {
|
|
20
|
+
return {
|
|
21
|
+
status: 500,
|
|
22
|
+
message: fallback
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (!error.response) {
|
|
27
|
+
return {
|
|
28
|
+
status: 502,
|
|
29
|
+
message: `Upstream request failed (${error.code || "network_error"}): ${error.message || fallback}`
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const status = error.response.status || 500;
|
|
34
|
+
const data = error.response?.data;
|
|
35
|
+
|
|
36
|
+
if (typeof data === "string" && data.trim()) {
|
|
37
|
+
return { status, message: data };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (data && typeof data === "object") {
|
|
41
|
+
const obj = data as Record<string, unknown>;
|
|
42
|
+
const message =
|
|
43
|
+
(typeof obj.error === "string" && obj.error) ||
|
|
44
|
+
(typeof obj.message === "string" && obj.message) ||
|
|
45
|
+
(typeof obj.msg === "string" && obj.msg) ||
|
|
46
|
+
error.message ||
|
|
47
|
+
fallback;
|
|
48
|
+
|
|
49
|
+
return { status, message };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
status,
|
|
54
|
+
message: error.message || fallback
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function resolveProjectRef(payload: unknown): string {
|
|
59
|
+
if (!payload || typeof payload !== "object") {
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const record = payload as Record<string, unknown>;
|
|
64
|
+
|
|
65
|
+
if (typeof record.ref === "string" && record.ref.trim()) {
|
|
66
|
+
return record.ref.trim();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (typeof record.id === "string" && record.id.trim()) {
|
|
70
|
+
return record.id.trim();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (typeof record.project_ref === "string" && record.project_ref.trim()) {
|
|
74
|
+
return record.project_ref.trim();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function fetchProjectAnonKey(
|
|
81
|
+
authHeader: string,
|
|
82
|
+
projectRef: string,
|
|
83
|
+
options: { attempts?: number; waitMs?: number } = {}
|
|
84
|
+
): Promise<string> {
|
|
85
|
+
const attempts = options.attempts ?? 1;
|
|
86
|
+
const waitMs = options.waitMs ?? 3000;
|
|
87
|
+
|
|
88
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
89
|
+
if (attempt > 1) {
|
|
90
|
+
await sleep(waitMs);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const keysResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}/api-keys`, {
|
|
95
|
+
headers: {
|
|
96
|
+
Authorization: authHeader
|
|
97
|
+
},
|
|
98
|
+
timeout: 10_000
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
const keys = keysResponse.data;
|
|
102
|
+
if (Array.isArray(keys)) {
|
|
103
|
+
const anon = keys.find((item) => item?.name === "anon")?.api_key;
|
|
104
|
+
if (typeof anon === "string" && anon.trim()) {
|
|
105
|
+
return anon;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// ignore and retry
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return "";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
router.post(
|
|
117
|
+
"/test-supabase",
|
|
118
|
+
validateBody(schemas.testSupabase),
|
|
119
|
+
asyncHandler(async (req, res) => {
|
|
120
|
+
const { url, anonKey } = req.body;
|
|
121
|
+
|
|
122
|
+
const supabase = createClient(url, anonKey, {
|
|
123
|
+
auth: {
|
|
124
|
+
autoRefreshToken: false,
|
|
125
|
+
persistSession: false
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const { error } = await supabase.from("user_settings").select("id").limit(1);
|
|
130
|
+
|
|
131
|
+
if (error && error.code !== "PGRST116") {
|
|
132
|
+
res.status(400).json({
|
|
133
|
+
valid: false,
|
|
134
|
+
message: error.message
|
|
135
|
+
});
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
res.json({
|
|
140
|
+
valid: true,
|
|
141
|
+
message: "Supabase connection verified"
|
|
142
|
+
});
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
router.get("/organizations", async (req, res) => {
|
|
147
|
+
const authHeader = req.headers.authorization;
|
|
148
|
+
if (!authHeader) {
|
|
149
|
+
res.status(401).json({ error: "Missing Authorization header" });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const response = await axios.get("https://api.supabase.com/v1/organizations", {
|
|
155
|
+
headers: {
|
|
156
|
+
Authorization: authHeader
|
|
157
|
+
},
|
|
158
|
+
timeout: 20_000
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
res.json(response.data);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
const extracted = extractAxiosError(error, "Failed to fetch organizations");
|
|
164
|
+
|
|
165
|
+
logger.warn("Organization fetch failed", {
|
|
166
|
+
status: extracted.status,
|
|
167
|
+
message: extracted.message
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
res.status(extracted.status).json({
|
|
171
|
+
error: extracted.message
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
router.get("/projects/:projectRef/credentials", async (req, res) => {
|
|
177
|
+
const authHeader = req.headers.authorization;
|
|
178
|
+
const projectRef = (req.params.projectRef || "").trim();
|
|
179
|
+
|
|
180
|
+
if (!authHeader) {
|
|
181
|
+
res.status(401).json({ error: "Missing Authorization header" });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (!projectRef) {
|
|
186
|
+
res.status(400).json({ error: "Missing projectRef" });
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const statusResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}`, {
|
|
192
|
+
headers: {
|
|
193
|
+
Authorization: authHeader
|
|
194
|
+
},
|
|
195
|
+
timeout: 10_000
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
const status = typeof statusResponse.data?.status === "string" ? statusResponse.data.status : "unknown";
|
|
199
|
+
const anonKey = await fetchProjectAnonKey(authHeader, projectRef, { attempts: 10, waitMs: 3000 });
|
|
200
|
+
|
|
201
|
+
if (!anonKey) {
|
|
202
|
+
res.status(425).json({
|
|
203
|
+
error: "Project exists, but anon API key is not ready yet. Retry shortly.",
|
|
204
|
+
status
|
|
205
|
+
});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
res.json({
|
|
210
|
+
projectId: projectRef,
|
|
211
|
+
status,
|
|
212
|
+
url: `https://${projectRef}.supabase.co`,
|
|
213
|
+
anonKey
|
|
214
|
+
});
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const extracted = extractAxiosError(error, "Failed to recover project credentials");
|
|
217
|
+
logger.warn("Project credential recovery failed", {
|
|
218
|
+
projectRef,
|
|
219
|
+
status: extracted.status,
|
|
220
|
+
message: extracted.message
|
|
221
|
+
});
|
|
222
|
+
res.status(extracted.status).json({
|
|
223
|
+
error: extracted.message
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
router.post("/auto-provision", validateBody(schemas.autoProvision), async (req, res) => {
|
|
229
|
+
const authHeader = req.headers.authorization;
|
|
230
|
+
const { orgId, projectName: requestedName, region: requestedRegion } = req.body;
|
|
231
|
+
|
|
232
|
+
if (!authHeader) {
|
|
233
|
+
res.status(401).json({ error: "Missing Authorization header" });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
res.setHeader("Content-Type", "text/event-stream");
|
|
238
|
+
res.setHeader("Cache-Control", "no-cache");
|
|
239
|
+
res.setHeader("Connection", "keep-alive");
|
|
240
|
+
// @ts-ignore express typings may not include flushHeaders depending on version
|
|
241
|
+
res.flushHeaders?.();
|
|
242
|
+
|
|
243
|
+
let disconnected = false;
|
|
244
|
+
res.on("close", () => {
|
|
245
|
+
disconnected = true;
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const sendEvent = (type: string, data: unknown) => {
|
|
249
|
+
if (disconnected || res.writableEnded) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
res.write(`data: ${JSON.stringify({ type, data })}\n\n`);
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
try {
|
|
256
|
+
const projectName = requestedName?.trim() || `Folio-${randomBytes(2).toString("hex")}`;
|
|
257
|
+
const region = requestedRegion || "us-east-1";
|
|
258
|
+
|
|
259
|
+
const dbPass =
|
|
260
|
+
randomBytes(16)
|
|
261
|
+
.toString("base64")
|
|
262
|
+
.replace(/\+/g, "a")
|
|
263
|
+
.replace(/\//g, "b")
|
|
264
|
+
.replace(/=/g, "c") + "1!Aa";
|
|
265
|
+
|
|
266
|
+
sendEvent("info", `Creating project ${projectName} in ${region}...`);
|
|
267
|
+
|
|
268
|
+
const createResponse = await axios.post(
|
|
269
|
+
"https://api.supabase.com/v1/projects",
|
|
270
|
+
{
|
|
271
|
+
name: projectName,
|
|
272
|
+
organization_id: orgId,
|
|
273
|
+
region,
|
|
274
|
+
db_pass: dbPass
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
headers: {
|
|
278
|
+
Authorization: authHeader
|
|
279
|
+
},
|
|
280
|
+
timeout: 20_000
|
|
281
|
+
}
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
const projectRef = resolveProjectRef(createResponse.data);
|
|
285
|
+
if (!projectRef) {
|
|
286
|
+
throw new Error("Project creation returned no project id");
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
sendEvent("project_id", projectRef);
|
|
290
|
+
sendEvent("info", `Project created (${projectRef}). Waiting for readiness...`);
|
|
291
|
+
|
|
292
|
+
let isReady = false;
|
|
293
|
+
const maxAttempts = 60;
|
|
294
|
+
|
|
295
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
|
296
|
+
await sleep(5000);
|
|
297
|
+
|
|
298
|
+
try {
|
|
299
|
+
const statusResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}`, {
|
|
300
|
+
headers: {
|
|
301
|
+
Authorization: authHeader
|
|
302
|
+
},
|
|
303
|
+
timeout: 10_000
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
const status = statusResponse.data?.status;
|
|
307
|
+
sendEvent("info", `Status: ${status || "unknown"} (${attempt}/${maxAttempts})`);
|
|
308
|
+
|
|
309
|
+
if (status === "ACTIVE" || status === "ACTIVE_HEALTHY") {
|
|
310
|
+
isReady = true;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
sendEvent("info", `Status check retry (${attempt}/${maxAttempts})...`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!isReady) {
|
|
319
|
+
throw new Error("Project provisioning timed out");
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
sendEvent("info", "Retrieving API keys...");
|
|
323
|
+
|
|
324
|
+
let anonKey = "";
|
|
325
|
+
for (let attempt = 1; attempt <= 10; attempt += 1) {
|
|
326
|
+
anonKey = await fetchProjectAnonKey(authHeader, projectRef);
|
|
327
|
+
if (anonKey) {
|
|
328
|
+
break;
|
|
329
|
+
}
|
|
330
|
+
sendEvent("info", `API keys not ready (${attempt}/10), retrying...`);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!anonKey) {
|
|
334
|
+
throw new Error("Could not retrieve anon key for project");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const supabaseUrl = `https://${projectRef}.supabase.co`;
|
|
338
|
+
|
|
339
|
+
sendEvent("info", "Waiting for DNS propagation...");
|
|
340
|
+
let dnsReady = false;
|
|
341
|
+
for (let attempt = 1; attempt <= 20; attempt += 1) {
|
|
342
|
+
try {
|
|
343
|
+
const ping = await axios.get(`${supabaseUrl}/rest/v1/`, {
|
|
344
|
+
timeout: 5_000,
|
|
345
|
+
validateStatus: () => true
|
|
346
|
+
});
|
|
347
|
+
if (ping.status < 500) {
|
|
348
|
+
dnsReady = true;
|
|
349
|
+
break;
|
|
350
|
+
}
|
|
351
|
+
} catch {
|
|
352
|
+
// ignore and retry
|
|
353
|
+
}
|
|
354
|
+
if (attempt % 5 === 0) {
|
|
355
|
+
sendEvent("info", "DNS still propagating...");
|
|
356
|
+
}
|
|
357
|
+
await sleep(3000);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!dnsReady) {
|
|
361
|
+
sendEvent("info", "DNS check timed out, continuing anyway.");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
sendEvent("success", {
|
|
365
|
+
url: supabaseUrl,
|
|
366
|
+
anonKey,
|
|
367
|
+
projectId: projectRef,
|
|
368
|
+
dbPass
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
sendEvent("done", "success");
|
|
372
|
+
} catch (error) {
|
|
373
|
+
const extracted = extractAxiosError(error, "Auto-provisioning failed");
|
|
374
|
+
|
|
375
|
+
logger.error("Auto-provision failed", {
|
|
376
|
+
status: extracted.status,
|
|
377
|
+
message: extracted.message
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
sendEvent("error", extracted.message);
|
|
381
|
+
sendEvent("done", "failed");
|
|
382
|
+
} finally {
|
|
383
|
+
if (!res.writableEnded) {
|
|
384
|
+
res.end();
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
export default router;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
import { optionalAuth } from "../middleware/auth.js";
|
|
3
|
+
|
|
4
|
+
const router = Router();
|
|
5
|
+
|
|
6
|
+
// Dashboard stats require authentication
|
|
7
|
+
router.use(optionalAuth);
|
|
8
|
+
|
|
9
|
+
export interface DashboardStats {
|
|
10
|
+
totalDocuments: number;
|
|
11
|
+
activePolicies: number;
|
|
12
|
+
ragChunks: number;
|
|
13
|
+
automationRuns: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
router.get("/", async (req, res) => {
|
|
17
|
+
if (!req.user || !req.supabase) {
|
|
18
|
+
res.status(401).json({ success: false, error: "Unauthorized" });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const userId = req.user.id;
|
|
24
|
+
const s = req.supabase; // the scoped service client
|
|
25
|
+
|
|
26
|
+
// 1. Total Documents Ingested
|
|
27
|
+
const { count: totalDocumentsCount, error: err1 } = await s
|
|
28
|
+
.from("ingestions")
|
|
29
|
+
.select("*", { count: 'exact', head: true })
|
|
30
|
+
.eq("user_id", userId);
|
|
31
|
+
|
|
32
|
+
// 2. Active Policies
|
|
33
|
+
const { count: activePoliciesCount, error: err2 } = await s
|
|
34
|
+
.from("policies")
|
|
35
|
+
.select("*", { count: 'exact', head: true })
|
|
36
|
+
.eq("user_id", userId)
|
|
37
|
+
.eq("enabled", true);
|
|
38
|
+
|
|
39
|
+
// 3. RAG Knowledge Base (Chunks)
|
|
40
|
+
const { count: ragChunksCount, error: err3 } = await s
|
|
41
|
+
.from("document_chunks")
|
|
42
|
+
.select("*", { count: 'exact', head: true })
|
|
43
|
+
.eq("user_id", userId);
|
|
44
|
+
|
|
45
|
+
// 4. Automation Runs (Sum of actions taken across ingestions)
|
|
46
|
+
const { data: ingestionsWithActions, error: err4 } = await s
|
|
47
|
+
.from("ingestions")
|
|
48
|
+
.select("actions_taken")
|
|
49
|
+
.eq("user_id", userId)
|
|
50
|
+
.not("actions_taken", "is", null);
|
|
51
|
+
|
|
52
|
+
let automationRunsCount = 0;
|
|
53
|
+
if (ingestionsWithActions) {
|
|
54
|
+
for (const ing of ingestionsWithActions) {
|
|
55
|
+
if (Array.isArray(ing.actions_taken)) {
|
|
56
|
+
automationRunsCount += ing.actions_taken.length;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (err1 || err2 || err3 || err4) {
|
|
62
|
+
console.error("Stats fetching errors:", { err1, err2, err3, err4 });
|
|
63
|
+
// Don't fail completely if one fails, just return 0s or what we have
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const stats: DashboardStats = {
|
|
67
|
+
totalDocuments: totalDocumentsCount ?? 0,
|
|
68
|
+
activePolicies: activePoliciesCount ?? 0,
|
|
69
|
+
ragChunks: ragChunksCount ?? 0,
|
|
70
|
+
automationRuns: automationRunsCount
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
res.json({ success: true, stats });
|
|
74
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
75
|
+
} catch (error: any) {
|
|
76
|
+
console.error("Dashboard Stats Route Error:", error);
|
|
77
|
+
res.status(500).json({ success: false, error: error.message || "Failed to fetch dashboard stats" });
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
export default router;
|