@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.
Files changed (163) hide show
  1. package/.env.example +20 -0
  2. package/README.md +63 -0
  3. package/api/server.ts +130 -0
  4. package/api/src/config/index.ts +96 -0
  5. package/api/src/middleware/auth.ts +128 -0
  6. package/api/src/middleware/errorHandler.ts +88 -0
  7. package/api/src/middleware/index.ts +4 -0
  8. package/api/src/middleware/rateLimit.ts +71 -0
  9. package/api/src/middleware/validation.ts +58 -0
  10. package/api/src/routes/accounts.ts +142 -0
  11. package/api/src/routes/baseline-config.ts +124 -0
  12. package/api/src/routes/chat.ts +154 -0
  13. package/api/src/routes/health.ts +61 -0
  14. package/api/src/routes/index.ts +35 -0
  15. package/api/src/routes/ingestions.ts +275 -0
  16. package/api/src/routes/migrate.ts +112 -0
  17. package/api/src/routes/policies.ts +121 -0
  18. package/api/src/routes/processing.ts +90 -0
  19. package/api/src/routes/rules.ts +11 -0
  20. package/api/src/routes/sdk.ts +100 -0
  21. package/api/src/routes/settings.ts +80 -0
  22. package/api/src/routes/setup.ts +389 -0
  23. package/api/src/routes/stats.ts +81 -0
  24. package/api/src/routes/tts.ts +190 -0
  25. package/api/src/services/BaselineConfigService.ts +208 -0
  26. package/api/src/services/ChatService.ts +204 -0
  27. package/api/src/services/GoogleDriveService.ts +331 -0
  28. package/api/src/services/GoogleSheetsService.ts +1107 -0
  29. package/api/src/services/IngestionService.ts +1187 -0
  30. package/api/src/services/ModelCapabilityService.ts +248 -0
  31. package/api/src/services/PolicyEngine.ts +1625 -0
  32. package/api/src/services/PolicyLearningService.ts +527 -0
  33. package/api/src/services/PolicyLoader.ts +249 -0
  34. package/api/src/services/RAGService.ts +391 -0
  35. package/api/src/services/SDKService.ts +249 -0
  36. package/api/src/services/supabase.ts +113 -0
  37. package/api/src/utils/Actuator.ts +284 -0
  38. package/api/src/utils/actions/ActionHandler.ts +34 -0
  39. package/api/src/utils/actions/AppendToGSheetAction.ts +260 -0
  40. package/api/src/utils/actions/AutoRenameAction.ts +58 -0
  41. package/api/src/utils/actions/CopyAction.ts +120 -0
  42. package/api/src/utils/actions/CopyToGDriveAction.ts +64 -0
  43. package/api/src/utils/actions/LogCsvAction.ts +48 -0
  44. package/api/src/utils/actions/NotifyAction.ts +39 -0
  45. package/api/src/utils/actions/RenameAction.ts +57 -0
  46. package/api/src/utils/actions/WebhookAction.ts +58 -0
  47. package/api/src/utils/actions/utils.ts +293 -0
  48. package/api/src/utils/llmResponse.ts +61 -0
  49. package/api/src/utils/logger.ts +67 -0
  50. package/bin/folio-deploy.js +12 -0
  51. package/bin/folio-setup.js +45 -0
  52. package/bin/folio.js +65 -0
  53. package/dist/api/server.js +106 -0
  54. package/dist/api/src/config/index.js +81 -0
  55. package/dist/api/src/middleware/auth.js +93 -0
  56. package/dist/api/src/middleware/errorHandler.js +73 -0
  57. package/dist/api/src/middleware/index.js +4 -0
  58. package/dist/api/src/middleware/rateLimit.js +43 -0
  59. package/dist/api/src/middleware/validation.js +54 -0
  60. package/dist/api/src/routes/accounts.js +110 -0
  61. package/dist/api/src/routes/baseline-config.js +91 -0
  62. package/dist/api/src/routes/chat.js +114 -0
  63. package/dist/api/src/routes/health.js +52 -0
  64. package/dist/api/src/routes/index.js +31 -0
  65. package/dist/api/src/routes/ingestions.js +207 -0
  66. package/dist/api/src/routes/migrate.js +91 -0
  67. package/dist/api/src/routes/policies.js +86 -0
  68. package/dist/api/src/routes/processing.js +75 -0
  69. package/dist/api/src/routes/rules.js +8 -0
  70. package/dist/api/src/routes/sdk.js +80 -0
  71. package/dist/api/src/routes/settings.js +68 -0
  72. package/dist/api/src/routes/setup.js +315 -0
  73. package/dist/api/src/routes/stats.js +62 -0
  74. package/dist/api/src/routes/tts.js +178 -0
  75. package/dist/api/src/services/BaselineConfigService.js +168 -0
  76. package/dist/api/src/services/ChatService.js +166 -0
  77. package/dist/api/src/services/GoogleDriveService.js +280 -0
  78. package/dist/api/src/services/GoogleSheetsService.js +795 -0
  79. package/dist/api/src/services/IngestionService.js +990 -0
  80. package/dist/api/src/services/ModelCapabilityService.js +179 -0
  81. package/dist/api/src/services/PolicyEngine.js +1353 -0
  82. package/dist/api/src/services/PolicyLearningService.js +397 -0
  83. package/dist/api/src/services/PolicyLoader.js +159 -0
  84. package/dist/api/src/services/RAGService.js +295 -0
  85. package/dist/api/src/services/SDKService.js +212 -0
  86. package/dist/api/src/services/supabase.js +72 -0
  87. package/dist/api/src/utils/Actuator.js +225 -0
  88. package/dist/api/src/utils/actions/ActionHandler.js +1 -0
  89. package/dist/api/src/utils/actions/AppendToGSheetAction.js +191 -0
  90. package/dist/api/src/utils/actions/AutoRenameAction.js +49 -0
  91. package/dist/api/src/utils/actions/CopyAction.js +112 -0
  92. package/dist/api/src/utils/actions/CopyToGDriveAction.js +55 -0
  93. package/dist/api/src/utils/actions/LogCsvAction.js +42 -0
  94. package/dist/api/src/utils/actions/NotifyAction.js +32 -0
  95. package/dist/api/src/utils/actions/RenameAction.js +51 -0
  96. package/dist/api/src/utils/actions/WebhookAction.js +51 -0
  97. package/dist/api/src/utils/actions/utils.js +237 -0
  98. package/dist/api/src/utils/llmResponse.js +63 -0
  99. package/dist/api/src/utils/logger.js +51 -0
  100. package/dist/assets/index-DzN8-j-e.css +1 -0
  101. package/dist/assets/index-Uy-ai3Dh.js +113 -0
  102. package/dist/favicon.svg +31 -0
  103. package/dist/folio-logo.svg +46 -0
  104. package/dist/index.html +14 -0
  105. package/docs-dev/FPE-spec.md +196 -0
  106. package/docs-dev/folio-prd.md +47 -0
  107. package/docs-dev/foundation-checklist.md +30 -0
  108. package/docs-dev/hybrid-routing-architecture.md +205 -0
  109. package/docs-dev/ingestion-engine.md +69 -0
  110. package/docs-dev/port-from-email-automator.md +32 -0
  111. package/docs-dev/tech-spec.md +98 -0
  112. package/index.html +13 -0
  113. package/package.json +101 -0
  114. package/public/favicon.svg +31 -0
  115. package/public/folio-logo.svg +46 -0
  116. package/scripts/dev-task.mjs +51 -0
  117. package/scripts/get-latest-migration-timestamp.mjs +34 -0
  118. package/scripts/migrate.sh +91 -0
  119. package/supabase/.temp/cli-latest +1 -0
  120. package/supabase/.temp/gotrue-version +1 -0
  121. package/supabase/.temp/pooler-url +1 -0
  122. package/supabase/.temp/postgres-version +1 -0
  123. package/supabase/.temp/project-ref +1 -0
  124. package/supabase/.temp/rest-version +1 -0
  125. package/supabase/.temp/storage-migration +1 -0
  126. package/supabase/.temp/storage-version +1 -0
  127. package/supabase/config.toml +64 -0
  128. package/supabase/functions/_shared/auth.ts +35 -0
  129. package/supabase/functions/_shared/cors.ts +12 -0
  130. package/supabase/functions/_shared/supabaseAdmin.ts +17 -0
  131. package/supabase/functions/api-v1-settings/index.ts +66 -0
  132. package/supabase/functions/setup/index.ts +91 -0
  133. package/supabase/migrations/20260223000000_initial_foundation.sql +136 -0
  134. package/supabase/migrations/20260223000001_add_migration_rpc.sql +10 -0
  135. package/supabase/migrations/20260224000002_add_init_state_view.sql +20 -0
  136. package/supabase/migrations/20260224000003_port_user_creation_parity.sql +139 -0
  137. package/supabase/migrations/20260224000004_add_avatars_storage.sql +26 -0
  138. package/supabase/migrations/20260224000005_add_tts_and_embed_settings.sql +24 -0
  139. package/supabase/migrations/20260224000006_add_policies_table.sql +48 -0
  140. package/supabase/migrations/20260224000007_fix_migration_rpc.sql +9 -0
  141. package/supabase/migrations/20260224000008_add_ingestions_table.sql +42 -0
  142. package/supabase/migrations/20260225000000_setup_compatible_mode.sql +119 -0
  143. package/supabase/migrations/20260225000001_restore_ingestions.sql +49 -0
  144. package/supabase/migrations/20260225000002_add_ingestion_trace.sql +2 -0
  145. package/supabase/migrations/20260225000003_add_baseline_configs.sql +35 -0
  146. package/supabase/migrations/20260226000000_add_processing_events.sql +26 -0
  147. package/supabase/migrations/20260226000001_add_ingestion_file_hash.sql +10 -0
  148. package/supabase/migrations/20260226000002_add_dynamic_rag.sql +150 -0
  149. package/supabase/migrations/20260226000003_add_ingestion_summary.sql +4 -0
  150. package/supabase/migrations/20260226000004_add_ingestion_tags.sql +7 -0
  151. package/supabase/migrations/20260226000005_add_chat_tables.sql +60 -0
  152. package/supabase/migrations/20260227000000_harden_chat_messages_rls.sql +25 -0
  153. package/supabase/migrations/20260228000000_add_vision_model_capabilities.sql +8 -0
  154. package/supabase/migrations/20260228000001_add_policy_match_feedback.sql +51 -0
  155. package/supabase/migrations/29991231235959_test_migration.sql +0 -0
  156. package/supabase/templates/confirmation.html +76 -0
  157. package/supabase/templates/email-change.html +76 -0
  158. package/supabase/templates/invite.html +72 -0
  159. package/supabase/templates/magic-link.html +68 -0
  160. package/supabase/templates/recovery.html +82 -0
  161. package/tsconfig.api.json +16 -0
  162. package/tsconfig.json +25 -0
  163. 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;