@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,80 @@
1
+ import { Router } from "express";
2
+ import { SDKService } from "../services/SDKService.js";
3
+ import { createLogger } from "../utils/logger.js";
4
+ import { extractLlmResponse, previewLlmText } from "../utils/llmResponse.js";
5
+ const router = Router();
6
+ const logger = createLogger("SDKRoutes");
7
+ /**
8
+ * GET /api/sdk/providers/chat
9
+ * Returns available chat providers and their models
10
+ */
11
+ router.get("/providers/chat", async (req, res) => {
12
+ try {
13
+ const sdk = SDKService.getSDK();
14
+ if (!sdk) {
15
+ return res.json({ success: false, message: "SDK not available", providers: [] });
16
+ }
17
+ const { providers } = await SDKService.withTimeout(sdk.llm.chatProviders(), 30000, "Chat providers fetch timed out");
18
+ res.json({ success: true, providers: providers || [] });
19
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
20
+ }
21
+ catch (error) {
22
+ res.json({ success: false, providers: [], message: error.message });
23
+ }
24
+ });
25
+ /**
26
+ * GET /api/sdk/providers/embed
27
+ * Returns available embedding providers and their models
28
+ */
29
+ router.get("/providers/embed", async (req, res) => {
30
+ try {
31
+ const sdk = SDKService.getSDK();
32
+ if (!sdk) {
33
+ return res.json({ success: false, message: "SDK not available", providers: [] });
34
+ }
35
+ const { providers } = await SDKService.withTimeout(sdk.llm.embedProviders(), 30000, "Embed providers fetch timed out");
36
+ res.json({ success: true, providers: providers || [] });
37
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
38
+ }
39
+ catch (error) {
40
+ res.json({ success: false, providers: [], message: error.message });
41
+ }
42
+ });
43
+ /**
44
+ * POST /api/sdk/test-llm
45
+ * Tests connection to a specific LLM provider/model
46
+ */
47
+ router.post("/test-llm", async (req, res) => {
48
+ try {
49
+ const { llm_provider, llm_model } = req.body;
50
+ const sdk = SDKService.getSDK();
51
+ if (!sdk) {
52
+ return res.json({ success: false, message: "SDK not available" });
53
+ }
54
+ const { provider, model } = await SDKService.resolveChatProvider({
55
+ llm_provider,
56
+ llm_model
57
+ });
58
+ logger.info("LLM request (sdk test-llm)", { provider, model });
59
+ const response = await sdk.llm.chat([{ role: "user", content: "Say OK" }], { provider, model });
60
+ const raw = extractLlmResponse(response);
61
+ logger.info("LLM response (sdk test-llm)", {
62
+ provider,
63
+ model,
64
+ raw_length: raw.length,
65
+ raw_preview: previewLlmText(raw),
66
+ });
67
+ if (response.success) {
68
+ res.json({ success: true, message: `Connected to ${provider}/${model}` });
69
+ }
70
+ else {
71
+ res.json({ success: false, message: response.error || "Failed to connect to LLM" });
72
+ }
73
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
+ }
75
+ catch (error) {
76
+ logger.error("LLM sdk test failed", { error: error?.message ?? String(error) });
77
+ res.json({ success: false, message: error.message });
78
+ }
79
+ });
80
+ export default router;
@@ -0,0 +1,68 @@
1
+ import { Router } from "express";
2
+ import { asyncHandler } from "../middleware/errorHandler.js";
3
+ import { optionalAuth } from "../middleware/auth.js";
4
+ const router = Router();
5
+ router.use(optionalAuth);
6
+ router.get("/", asyncHandler(async (req, res) => {
7
+ if (!req.supabase || !req.user) {
8
+ res.status(401).json({ error: "Authentication required" });
9
+ return;
10
+ }
11
+ const { data, error } = await req.supabase
12
+ .from("user_settings")
13
+ .select("*")
14
+ .eq("user_id", req.user.id)
15
+ .maybeSingle();
16
+ if (error) {
17
+ res.status(500).json({ error: error.message });
18
+ return;
19
+ }
20
+ res.json({ settings: data });
21
+ }));
22
+ router.patch("/", asyncHandler(async (req, res) => {
23
+ if (!req.supabase || !req.user) {
24
+ res.status(401).json({ error: "Authentication required" });
25
+ return;
26
+ }
27
+ const body = req.body;
28
+ const rawVisionMap = body.vision_model_capabilities;
29
+ const payload = {
30
+ llm_provider: body.llm_provider,
31
+ llm_model: body.llm_model,
32
+ sync_interval_minutes: body.sync_interval_minutes,
33
+ tts_auto_play: body.tts_auto_play,
34
+ tts_provider: body.tts_provider,
35
+ tts_voice: body.tts_voice,
36
+ tts_speed: body.tts_speed,
37
+ tts_quality: body.tts_quality,
38
+ embedding_provider: body.embedding_provider,
39
+ embedding_model: body.embedding_model,
40
+ storage_path: body.storage_path,
41
+ vision_model_capabilities: rawVisionMap && typeof rawVisionMap === "object" && !Array.isArray(rawVisionMap)
42
+ ? rawVisionMap
43
+ : undefined,
44
+ google_client_id: body.google_client_id,
45
+ google_client_secret: body.google_client_secret,
46
+ microsoft_client_id: body.microsoft_client_id,
47
+ microsoft_tenant_id: body.microsoft_tenant_id
48
+ };
49
+ // Remove undefined fields
50
+ Object.keys(payload).forEach(key => {
51
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
52
+ if (payload[key] === undefined) {
53
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
54
+ delete payload[key];
55
+ }
56
+ });
57
+ const { data, error } = await req.supabase
58
+ .from("user_settings")
59
+ .upsert({ user_id: req.user.id, ...payload }, { onConflict: "user_id" })
60
+ .select("*")
61
+ .single();
62
+ if (error) {
63
+ res.status(500).json({ error: error.message });
64
+ return;
65
+ }
66
+ res.json({ settings: data });
67
+ }));
68
+ export default router;
@@ -0,0 +1,315 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import axios from "axios";
3
+ import { createClient } from "@supabase/supabase-js";
4
+ import { Router } from "express";
5
+ import { asyncHandler } from "../middleware/errorHandler.js";
6
+ import { createLogger } from "../utils/logger.js";
7
+ import { schemas, validateBody } from "../middleware/validation.js";
8
+ const router = Router();
9
+ const logger = createLogger("SetupRoutes");
10
+ function sleep(ms) {
11
+ return new Promise((resolve) => setTimeout(resolve, ms));
12
+ }
13
+ function extractAxiosError(error, fallback) {
14
+ if (!axios.isAxiosError(error)) {
15
+ return {
16
+ status: 500,
17
+ message: fallback
18
+ };
19
+ }
20
+ if (!error.response) {
21
+ return {
22
+ status: 502,
23
+ message: `Upstream request failed (${error.code || "network_error"}): ${error.message || fallback}`
24
+ };
25
+ }
26
+ const status = error.response.status || 500;
27
+ const data = error.response?.data;
28
+ if (typeof data === "string" && data.trim()) {
29
+ return { status, message: data };
30
+ }
31
+ if (data && typeof data === "object") {
32
+ const obj = data;
33
+ const message = (typeof obj.error === "string" && obj.error) ||
34
+ (typeof obj.message === "string" && obj.message) ||
35
+ (typeof obj.msg === "string" && obj.msg) ||
36
+ error.message ||
37
+ fallback;
38
+ return { status, message };
39
+ }
40
+ return {
41
+ status,
42
+ message: error.message || fallback
43
+ };
44
+ }
45
+ function resolveProjectRef(payload) {
46
+ if (!payload || typeof payload !== "object") {
47
+ return "";
48
+ }
49
+ const record = payload;
50
+ if (typeof record.ref === "string" && record.ref.trim()) {
51
+ return record.ref.trim();
52
+ }
53
+ if (typeof record.id === "string" && record.id.trim()) {
54
+ return record.id.trim();
55
+ }
56
+ if (typeof record.project_ref === "string" && record.project_ref.trim()) {
57
+ return record.project_ref.trim();
58
+ }
59
+ return "";
60
+ }
61
+ async function fetchProjectAnonKey(authHeader, projectRef, options = {}) {
62
+ const attempts = options.attempts ?? 1;
63
+ const waitMs = options.waitMs ?? 3000;
64
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
65
+ if (attempt > 1) {
66
+ await sleep(waitMs);
67
+ }
68
+ try {
69
+ const keysResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}/api-keys`, {
70
+ headers: {
71
+ Authorization: authHeader
72
+ },
73
+ timeout: 10_000
74
+ });
75
+ const keys = keysResponse.data;
76
+ if (Array.isArray(keys)) {
77
+ const anon = keys.find((item) => item?.name === "anon")?.api_key;
78
+ if (typeof anon === "string" && anon.trim()) {
79
+ return anon;
80
+ }
81
+ }
82
+ }
83
+ catch {
84
+ // ignore and retry
85
+ }
86
+ }
87
+ return "";
88
+ }
89
+ router.post("/test-supabase", validateBody(schemas.testSupabase), asyncHandler(async (req, res) => {
90
+ const { url, anonKey } = req.body;
91
+ const supabase = createClient(url, anonKey, {
92
+ auth: {
93
+ autoRefreshToken: false,
94
+ persistSession: false
95
+ }
96
+ });
97
+ const { error } = await supabase.from("user_settings").select("id").limit(1);
98
+ if (error && error.code !== "PGRST116") {
99
+ res.status(400).json({
100
+ valid: false,
101
+ message: error.message
102
+ });
103
+ return;
104
+ }
105
+ res.json({
106
+ valid: true,
107
+ message: "Supabase connection verified"
108
+ });
109
+ }));
110
+ router.get("/organizations", async (req, res) => {
111
+ const authHeader = req.headers.authorization;
112
+ if (!authHeader) {
113
+ res.status(401).json({ error: "Missing Authorization header" });
114
+ return;
115
+ }
116
+ try {
117
+ const response = await axios.get("https://api.supabase.com/v1/organizations", {
118
+ headers: {
119
+ Authorization: authHeader
120
+ },
121
+ timeout: 20_000
122
+ });
123
+ res.json(response.data);
124
+ }
125
+ catch (error) {
126
+ const extracted = extractAxiosError(error, "Failed to fetch organizations");
127
+ logger.warn("Organization fetch failed", {
128
+ status: extracted.status,
129
+ message: extracted.message
130
+ });
131
+ res.status(extracted.status).json({
132
+ error: extracted.message
133
+ });
134
+ }
135
+ });
136
+ router.get("/projects/:projectRef/credentials", async (req, res) => {
137
+ const authHeader = req.headers.authorization;
138
+ const projectRef = (req.params.projectRef || "").trim();
139
+ if (!authHeader) {
140
+ res.status(401).json({ error: "Missing Authorization header" });
141
+ return;
142
+ }
143
+ if (!projectRef) {
144
+ res.status(400).json({ error: "Missing projectRef" });
145
+ return;
146
+ }
147
+ try {
148
+ const statusResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}`, {
149
+ headers: {
150
+ Authorization: authHeader
151
+ },
152
+ timeout: 10_000
153
+ });
154
+ const status = typeof statusResponse.data?.status === "string" ? statusResponse.data.status : "unknown";
155
+ const anonKey = await fetchProjectAnonKey(authHeader, projectRef, { attempts: 10, waitMs: 3000 });
156
+ if (!anonKey) {
157
+ res.status(425).json({
158
+ error: "Project exists, but anon API key is not ready yet. Retry shortly.",
159
+ status
160
+ });
161
+ return;
162
+ }
163
+ res.json({
164
+ projectId: projectRef,
165
+ status,
166
+ url: `https://${projectRef}.supabase.co`,
167
+ anonKey
168
+ });
169
+ }
170
+ catch (error) {
171
+ const extracted = extractAxiosError(error, "Failed to recover project credentials");
172
+ logger.warn("Project credential recovery failed", {
173
+ projectRef,
174
+ status: extracted.status,
175
+ message: extracted.message
176
+ });
177
+ res.status(extracted.status).json({
178
+ error: extracted.message
179
+ });
180
+ }
181
+ });
182
+ router.post("/auto-provision", validateBody(schemas.autoProvision), async (req, res) => {
183
+ const authHeader = req.headers.authorization;
184
+ const { orgId, projectName: requestedName, region: requestedRegion } = req.body;
185
+ if (!authHeader) {
186
+ res.status(401).json({ error: "Missing Authorization header" });
187
+ return;
188
+ }
189
+ res.setHeader("Content-Type", "text/event-stream");
190
+ res.setHeader("Cache-Control", "no-cache");
191
+ res.setHeader("Connection", "keep-alive");
192
+ // @ts-ignore express typings may not include flushHeaders depending on version
193
+ res.flushHeaders?.();
194
+ let disconnected = false;
195
+ res.on("close", () => {
196
+ disconnected = true;
197
+ });
198
+ const sendEvent = (type, data) => {
199
+ if (disconnected || res.writableEnded) {
200
+ return;
201
+ }
202
+ res.write(`data: ${JSON.stringify({ type, data })}\n\n`);
203
+ };
204
+ try {
205
+ const projectName = requestedName?.trim() || `Folio-${randomBytes(2).toString("hex")}`;
206
+ const region = requestedRegion || "us-east-1";
207
+ const dbPass = randomBytes(16)
208
+ .toString("base64")
209
+ .replace(/\+/g, "a")
210
+ .replace(/\//g, "b")
211
+ .replace(/=/g, "c") + "1!Aa";
212
+ sendEvent("info", `Creating project ${projectName} in ${region}...`);
213
+ const createResponse = await axios.post("https://api.supabase.com/v1/projects", {
214
+ name: projectName,
215
+ organization_id: orgId,
216
+ region,
217
+ db_pass: dbPass
218
+ }, {
219
+ headers: {
220
+ Authorization: authHeader
221
+ },
222
+ timeout: 20_000
223
+ });
224
+ const projectRef = resolveProjectRef(createResponse.data);
225
+ if (!projectRef) {
226
+ throw new Error("Project creation returned no project id");
227
+ }
228
+ sendEvent("project_id", projectRef);
229
+ sendEvent("info", `Project created (${projectRef}). Waiting for readiness...`);
230
+ let isReady = false;
231
+ const maxAttempts = 60;
232
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
233
+ await sleep(5000);
234
+ try {
235
+ const statusResponse = await axios.get(`https://api.supabase.com/v1/projects/${projectRef}`, {
236
+ headers: {
237
+ Authorization: authHeader
238
+ },
239
+ timeout: 10_000
240
+ });
241
+ const status = statusResponse.data?.status;
242
+ sendEvent("info", `Status: ${status || "unknown"} (${attempt}/${maxAttempts})`);
243
+ if (status === "ACTIVE" || status === "ACTIVE_HEALTHY") {
244
+ isReady = true;
245
+ break;
246
+ }
247
+ }
248
+ catch {
249
+ sendEvent("info", `Status check retry (${attempt}/${maxAttempts})...`);
250
+ }
251
+ }
252
+ if (!isReady) {
253
+ throw new Error("Project provisioning timed out");
254
+ }
255
+ sendEvent("info", "Retrieving API keys...");
256
+ let anonKey = "";
257
+ for (let attempt = 1; attempt <= 10; attempt += 1) {
258
+ anonKey = await fetchProjectAnonKey(authHeader, projectRef);
259
+ if (anonKey) {
260
+ break;
261
+ }
262
+ sendEvent("info", `API keys not ready (${attempt}/10), retrying...`);
263
+ }
264
+ if (!anonKey) {
265
+ throw new Error("Could not retrieve anon key for project");
266
+ }
267
+ const supabaseUrl = `https://${projectRef}.supabase.co`;
268
+ sendEvent("info", "Waiting for DNS propagation...");
269
+ let dnsReady = false;
270
+ for (let attempt = 1; attempt <= 20; attempt += 1) {
271
+ try {
272
+ const ping = await axios.get(`${supabaseUrl}/rest/v1/`, {
273
+ timeout: 5_000,
274
+ validateStatus: () => true
275
+ });
276
+ if (ping.status < 500) {
277
+ dnsReady = true;
278
+ break;
279
+ }
280
+ }
281
+ catch {
282
+ // ignore and retry
283
+ }
284
+ if (attempt % 5 === 0) {
285
+ sendEvent("info", "DNS still propagating...");
286
+ }
287
+ await sleep(3000);
288
+ }
289
+ if (!dnsReady) {
290
+ sendEvent("info", "DNS check timed out, continuing anyway.");
291
+ }
292
+ sendEvent("success", {
293
+ url: supabaseUrl,
294
+ anonKey,
295
+ projectId: projectRef,
296
+ dbPass
297
+ });
298
+ sendEvent("done", "success");
299
+ }
300
+ catch (error) {
301
+ const extracted = extractAxiosError(error, "Auto-provisioning failed");
302
+ logger.error("Auto-provision failed", {
303
+ status: extracted.status,
304
+ message: extracted.message
305
+ });
306
+ sendEvent("error", extracted.message);
307
+ sendEvent("done", "failed");
308
+ }
309
+ finally {
310
+ if (!res.writableEnded) {
311
+ res.end();
312
+ }
313
+ }
314
+ });
315
+ export default router;
@@ -0,0 +1,62 @@
1
+ import { Router } from "express";
2
+ import { optionalAuth } from "../middleware/auth.js";
3
+ const router = Router();
4
+ // Dashboard stats require authentication
5
+ router.use(optionalAuth);
6
+ router.get("/", async (req, res) => {
7
+ if (!req.user || !req.supabase) {
8
+ res.status(401).json({ success: false, error: "Unauthorized" });
9
+ return;
10
+ }
11
+ try {
12
+ const userId = req.user.id;
13
+ const s = req.supabase; // the scoped service client
14
+ // 1. Total Documents Ingested
15
+ const { count: totalDocumentsCount, error: err1 } = await s
16
+ .from("ingestions")
17
+ .select("*", { count: 'exact', head: true })
18
+ .eq("user_id", userId);
19
+ // 2. Active Policies
20
+ const { count: activePoliciesCount, error: err2 } = await s
21
+ .from("policies")
22
+ .select("*", { count: 'exact', head: true })
23
+ .eq("user_id", userId)
24
+ .eq("enabled", true);
25
+ // 3. RAG Knowledge Base (Chunks)
26
+ const { count: ragChunksCount, error: err3 } = await s
27
+ .from("document_chunks")
28
+ .select("*", { count: 'exact', head: true })
29
+ .eq("user_id", userId);
30
+ // 4. Automation Runs (Sum of actions taken across ingestions)
31
+ const { data: ingestionsWithActions, error: err4 } = await s
32
+ .from("ingestions")
33
+ .select("actions_taken")
34
+ .eq("user_id", userId)
35
+ .not("actions_taken", "is", null);
36
+ let automationRunsCount = 0;
37
+ if (ingestionsWithActions) {
38
+ for (const ing of ingestionsWithActions) {
39
+ if (Array.isArray(ing.actions_taken)) {
40
+ automationRunsCount += ing.actions_taken.length;
41
+ }
42
+ }
43
+ }
44
+ if (err1 || err2 || err3 || err4) {
45
+ console.error("Stats fetching errors:", { err1, err2, err3, err4 });
46
+ // Don't fail completely if one fails, just return 0s or what we have
47
+ }
48
+ const stats = {
49
+ totalDocuments: totalDocumentsCount ?? 0,
50
+ activePolicies: activePoliciesCount ?? 0,
51
+ ragChunks: ragChunksCount ?? 0,
52
+ automationRuns: automationRunsCount
53
+ };
54
+ res.json({ success: true, stats });
55
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
56
+ }
57
+ catch (error) {
58
+ console.error("Dashboard Stats Route Error:", error);
59
+ res.status(500).json({ success: false, error: error.message || "Failed to fetch dashboard stats" });
60
+ }
61
+ });
62
+ export default router;