@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,275 @@
1
+ import { Router } from "express";
2
+ import multer from "multer";
3
+ import os from "os";
4
+ import path from "path";
5
+ import fs from "fs/promises";
6
+ import crypto from "crypto";
7
+ import { asyncHandler } from "../middleware/errorHandler.js";
8
+ import { optionalAuth } from "../middleware/auth.js";
9
+ import { IngestionService } from "../services/IngestionService.js";
10
+
11
+ const router = Router();
12
+ const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 20 * 1024 * 1024 } });
13
+
14
+ router.use(optionalAuth);
15
+
16
+ // GET /api/ingestions — list ingestions (paginated, searchable)
17
+ router.get(
18
+ "/",
19
+ asyncHandler(async (req, res) => {
20
+ if (!req.supabase || !req.user) {
21
+ res.status(401).json({ success: false, error: "Authentication required" });
22
+ return;
23
+ }
24
+ const page = Math.max(1, parseInt(req.query["page"] as string) || 1);
25
+ const pageSize = Math.min(100, Math.max(1, parseInt(req.query["pageSize"] as string) || 20));
26
+ const query = (req.query["q"] as string | undefined)?.trim() || undefined;
27
+ const { ingestions, total } = await IngestionService.list(req.supabase, req.user.id, { page, pageSize, query });
28
+ res.json({ success: true, ingestions, total, page, pageSize });
29
+ })
30
+ );
31
+
32
+ // GET /api/ingestions/:id — get single ingestion
33
+ router.get(
34
+ "/:id",
35
+ asyncHandler(async (req, res) => {
36
+ if (!req.supabase || !req.user) {
37
+ res.status(401).json({ success: false, error: "Authentication required" });
38
+ return;
39
+ }
40
+ const ingestion = await IngestionService.get(req.params["id"] as string, req.supabase, req.user.id);
41
+ if (!ingestion) {
42
+ res.status(404).json({ success: false, error: "Not found" });
43
+ return;
44
+ }
45
+ res.json({ success: true, ingestion });
46
+ })
47
+ );
48
+
49
+ // POST /api/ingestions/upload — manual file upload
50
+ router.post(
51
+ "/upload",
52
+ upload.single("file"),
53
+ asyncHandler(async (req, res) => {
54
+ if (!req.supabase || !req.user) {
55
+ res.status(401).json({ success: false, error: "Authentication required" });
56
+ return;
57
+ }
58
+ const file = req.file;
59
+ if (!file) {
60
+ res.status(400).json({ success: false, error: "No file uploaded" });
61
+ return;
62
+ }
63
+
64
+ // Fetch user configured Storage Path (Dropzone)
65
+ const { data: settings } = await req.supabase
66
+ .from("user_settings")
67
+ .select("storage_path")
68
+ .eq("user_id", req.user.id)
69
+ .maybeSingle();
70
+
71
+ const dropzoneDir = settings?.storage_path || path.join(os.homedir(), ".realtimex", "folio", "dropzone");
72
+ await fs.mkdir(dropzoneDir, { recursive: true });
73
+
74
+ // Compute SHA-256 hash before writing — used for deduplication
75
+ const fileHash = crypto.createHash("sha256").update(file.buffer).digest("hex");
76
+
77
+ // Save physical file
78
+ const safeFilename = `${Date.now()}-${file.originalname.replace(/[^a-zA-Z0-9.-]/g, "_")}`;
79
+ const filePath = path.join(dropzoneDir, safeFilename);
80
+ await fs.writeFile(filePath, file.buffer);
81
+
82
+ // Extract text (mock extraction for pdfs, works for text files)
83
+ const content = file.buffer.toString("utf-8").replace(/\0/g, "").slice(0, 50_000);
84
+
85
+ const ingestion = await IngestionService.ingest({
86
+ supabase: req.supabase,
87
+ userId: req.user.id,
88
+ filename: file.originalname,
89
+ mimeType: file.mimetype,
90
+ fileSize: file.size,
91
+ source: "upload",
92
+ filePath,
93
+ content,
94
+ fileHash,
95
+ });
96
+
97
+ res.status(201).json({ success: true, ingestion });
98
+ })
99
+ );
100
+
101
+ // POST /api/ingestions/:id/rerun — re-run processing
102
+ router.post(
103
+ "/:id/rerun",
104
+ asyncHandler(async (req, res) => {
105
+ if (!req.supabase || !req.user) {
106
+ res.status(401).json({ success: false, error: "Authentication required" });
107
+ return;
108
+ }
109
+ const matched = await IngestionService.rerun(req.params["id"] as string, req.supabase, req.user.id);
110
+ res.json({ success: true, matched });
111
+ })
112
+ );
113
+
114
+ // POST /api/ingestions/:id/match — manually assign a policy and optionally learn from it
115
+ router.post(
116
+ "/:id/match",
117
+ asyncHandler(async (req, res) => {
118
+ if (!req.supabase || !req.user) {
119
+ res.status(401).json({ success: false, error: "Authentication required" });
120
+ return;
121
+ }
122
+
123
+ const policyId = typeof req.body?.policy_id === "string" ? req.body.policy_id.trim() : "";
124
+ if (!policyId) {
125
+ res.status(400).json({ success: false, error: "policy_id is required" });
126
+ return;
127
+ }
128
+
129
+ const learn = req.body?.learn !== false;
130
+ const rerun = req.body?.rerun !== false;
131
+ const allowSideEffects = req.body?.allow_side_effects === true;
132
+ try {
133
+ const ingestion = await IngestionService.matchToPolicy(
134
+ req.params["id"] as string,
135
+ policyId,
136
+ req.supabase,
137
+ req.user.id,
138
+ {
139
+ learn,
140
+ rerun,
141
+ allowSideEffects,
142
+ }
143
+ );
144
+ res.json({ success: true, ingestion });
145
+ } catch (error) {
146
+ const message = error instanceof Error ? error.message : String(error);
147
+ if (/not found/i.test(message)) {
148
+ res.status(404).json({ success: false, error: message });
149
+ return;
150
+ }
151
+ if (/required|processing|disabled|cannot manually match|side-effect/i.test(message)) {
152
+ res.status(400).json({ success: false, error: message });
153
+ return;
154
+ }
155
+ throw error;
156
+ }
157
+ })
158
+ );
159
+
160
+ // POST /api/ingestions/:id/refine-policy — suggest a refinement draft for a target policy
161
+ router.post(
162
+ "/:id/refine-policy",
163
+ asyncHandler(async (req, res) => {
164
+ if (!req.supabase || !req.user) {
165
+ res.status(401).json({ success: false, error: "Authentication required" });
166
+ return;
167
+ }
168
+
169
+ const policyId = typeof req.body?.policy_id === "string" ? req.body.policy_id.trim() : "";
170
+ if (!policyId) {
171
+ res.status(400).json({ success: false, error: "policy_id is required" });
172
+ return;
173
+ }
174
+
175
+ try {
176
+ const suggestion = await IngestionService.suggestPolicyRefinement(
177
+ req.params["id"] as string,
178
+ policyId,
179
+ req.supabase,
180
+ req.user.id,
181
+ {
182
+ provider: typeof req.body?.provider === "string" ? req.body.provider : undefined,
183
+ model: typeof req.body?.model === "string" ? req.body.model : undefined,
184
+ }
185
+ );
186
+ res.json({ success: true, suggestion });
187
+ } catch (error) {
188
+ const message = error instanceof Error ? error.message : String(error);
189
+ if (/not found/i.test(message)) {
190
+ res.status(404).json({ success: false, error: message });
191
+ return;
192
+ }
193
+ if (/required|disabled/i.test(message)) {
194
+ res.status(400).json({ success: false, error: message });
195
+ return;
196
+ }
197
+ throw error;
198
+ }
199
+ })
200
+ );
201
+
202
+ // POST /api/ingestions/:id/summarize — generate (or return cached) prose summary
203
+ router.post(
204
+ "/:id/summarize",
205
+ asyncHandler(async (req, res) => {
206
+ if (!req.supabase || !req.user) {
207
+ res.status(401).json({ success: false, error: "Authentication required" });
208
+ return;
209
+ }
210
+ const { data: settingsRow } = await req.supabase
211
+ .from("user_settings")
212
+ .select("llm_provider, llm_model")
213
+ .eq("user_id", req.user.id)
214
+ .maybeSingle();
215
+
216
+ const llmSettings = {
217
+ llm_provider: settingsRow?.llm_provider ?? undefined,
218
+ llm_model: settingsRow?.llm_model ?? undefined,
219
+ };
220
+
221
+ const summary = await IngestionService.summarize(
222
+ req.params["id"] as string,
223
+ req.supabase,
224
+ req.user.id,
225
+ llmSettings
226
+ );
227
+ res.json({ success: true, summary });
228
+ })
229
+ );
230
+
231
+ // PATCH /api/ingestions/:id/tags — replace tags array (human edits)
232
+ router.patch(
233
+ "/:id/tags",
234
+ asyncHandler(async (req, res) => {
235
+ if (!req.supabase || !req.user) {
236
+ res.status(401).json({ success: false, error: "Authentication required" });
237
+ return;
238
+ }
239
+ const tags: unknown = req.body?.tags;
240
+ if (!Array.isArray(tags) || tags.some((t) => typeof t !== "string")) {
241
+ res.status(400).json({ success: false, error: "tags must be an array of strings" });
242
+ return;
243
+ }
244
+ const normalized = (tags as string[]).map((t) => t.toLowerCase().trim()).filter(Boolean);
245
+ const { error } = await req.supabase
246
+ .from("ingestions")
247
+ .update({ tags: normalized })
248
+ .eq("id", req.params["id"] as string)
249
+ .eq("user_id", req.user.id);
250
+ if (error) {
251
+ res.status(500).json({ success: false, error: error.message });
252
+ return;
253
+ }
254
+ res.json({ success: true, tags: normalized });
255
+ })
256
+ );
257
+
258
+ // DELETE /api/ingestions/:id — delete
259
+ router.delete(
260
+ "/:id",
261
+ asyncHandler(async (req, res) => {
262
+ if (!req.supabase || !req.user) {
263
+ res.status(401).json({ success: false, error: "Authentication required" });
264
+ return;
265
+ }
266
+ const deleted = await IngestionService.delete(req.params["id"] as string, req.supabase, req.user.id);
267
+ if (!deleted) {
268
+ res.status(404).json({ success: false, error: "Not found" });
269
+ return;
270
+ }
271
+ res.json({ success: true });
272
+ })
273
+ );
274
+
275
+ export default router;
@@ -0,0 +1,112 @@
1
+ import { spawn } from "node:child_process";
2
+ import { join } from "node:path";
3
+
4
+ import { Router } from "express";
5
+
6
+ import { config } from "../config/index.js";
7
+ import { asyncHandler } from "../middleware/errorHandler.js";
8
+ import { schemas, validateBody } from "../middleware/validation.js";
9
+ import { createLogger } from "../utils/logger.js";
10
+
11
+ const router = Router();
12
+ const logger = createLogger("MigrateRoutes");
13
+
14
+ router.post(
15
+ "/",
16
+ validateBody(schemas.migrate),
17
+ asyncHandler(async (req, res) => {
18
+ const { projectRef, accessToken, anonKey } = req.body;
19
+
20
+ logger.info("Starting migration", { projectRef });
21
+
22
+ res.setHeader("Content-Type", "text/event-stream");
23
+ res.setHeader("Cache-Control", "no-cache");
24
+ res.setHeader("Connection", "keep-alive");
25
+ // @ts-ignore express typings may not include flushHeaders depending on version
26
+ res.flushHeaders?.();
27
+
28
+ const sendEvent = (type: string, data: string) => {
29
+ if (res.writableEnded) {
30
+ return;
31
+ }
32
+ res.write(`data: ${JSON.stringify({ type, data })}\n\n`);
33
+ };
34
+
35
+ const scriptPath = join(config.scriptsDir, "migrate.sh");
36
+
37
+ const child = spawn("bash", [scriptPath], {
38
+ env: {
39
+ ...process.env,
40
+ SUPABASE_PROJECT_ID: projectRef,
41
+ SUPABASE_ACCESS_TOKEN: accessToken,
42
+ SUPABASE_ANON_KEY: anonKey || "",
43
+ SKIP_FUNCTIONS: process.env.SKIP_FUNCTIONS || "0"
44
+ },
45
+ cwd: config.rootDir
46
+ });
47
+
48
+ let clientDisconnected = false;
49
+ const stopChild = () => {
50
+ clientDisconnected = true;
51
+ if (!child.killed) {
52
+ child.kill("SIGTERM");
53
+ }
54
+ };
55
+
56
+ res.on("close", stopChild);
57
+ req.on("aborted", stopChild);
58
+
59
+ child.stdout.on("data", (chunk) => {
60
+ const lines = String(chunk)
61
+ .split("\n")
62
+ .filter((line) => line.trim().length > 0);
63
+ for (const line of lines) {
64
+ sendEvent("stdout", line);
65
+ }
66
+ });
67
+
68
+ child.stderr.on("data", (chunk) => {
69
+ const lines = String(chunk)
70
+ .split("\n")
71
+ .filter((line) => line.trim().length > 0);
72
+ for (const line of lines) {
73
+ sendEvent("stderr", line);
74
+ }
75
+ });
76
+
77
+ child.on("error", (error) => {
78
+ if (clientDisconnected) {
79
+ logger.info("Migration process ended after client disconnect", { projectRef });
80
+ return;
81
+ }
82
+ sendEvent("error", `Failed to run migration: ${error.message}`);
83
+ sendEvent("done", "failed");
84
+ if (!res.writableEnded) {
85
+ res.end();
86
+ }
87
+ });
88
+
89
+ child.on("close", (code, signal) => {
90
+ if (clientDisconnected) {
91
+ logger.info("Migration process stopped after client disconnect", { projectRef, code, signal });
92
+ return;
93
+ }
94
+
95
+ if (code === 0) {
96
+ sendEvent("info", "Migration completed successfully.");
97
+ sendEvent("done", "success");
98
+ } else {
99
+ const codeLabel = code === null ? "null" : String(code);
100
+ const signalSuffix = signal ? ` (signal: ${signal})` : "";
101
+ sendEvent("error", `Migration failed with code ${codeLabel}${signalSuffix}`);
102
+ sendEvent("done", "failed");
103
+ }
104
+
105
+ if (!res.writableEnded) {
106
+ res.end();
107
+ }
108
+ });
109
+ })
110
+ );
111
+
112
+ export default router;
@@ -0,0 +1,121 @@
1
+ import { Router } from "express";
2
+ import { asyncHandler } from "../middleware/errorHandler.js";
3
+ import { optionalAuth } from "../middleware/auth.js";
4
+ import { PolicyLoader } from "../services/PolicyLoader.js";
5
+ import { PolicyEngine } from "../services/PolicyEngine.js";
6
+ import { PolicyLearningService } from "../services/PolicyLearningService.js";
7
+
8
+ const router = Router();
9
+
10
+ // Use optionalAuth on all routes — sets req.supabase + req.user if a valid
11
+ // bearer token is present. Falls through without error if no token.
12
+ router.use(optionalAuth);
13
+
14
+ // GET /api/policies — list all loaded policies
15
+ router.get(
16
+ "/",
17
+ asyncHandler(async (req, res) => {
18
+ const policies = await PolicyLoader.load(false, req.supabase);
19
+ if (!req.supabase || !req.user || policies.length === 0) {
20
+ res.json({ success: true, policies });
21
+ return;
22
+ }
23
+
24
+ const stats = await PolicyLearningService.getPolicyLearningStats({
25
+ supabase: req.supabase,
26
+ userId: req.user.id,
27
+ policyIds: policies.map((policy) => policy.metadata.id),
28
+ });
29
+
30
+ const enrichedPolicies = policies.map((policy) => {
31
+ const stat = stats[policy.metadata.id];
32
+ return {
33
+ ...policy,
34
+ metadata: {
35
+ ...policy.metadata,
36
+ learning_examples: stat?.samples ?? 0,
37
+ learning_last_at: stat?.lastSampleAt ?? null,
38
+ },
39
+ };
40
+ });
41
+
42
+ res.json({ success: true, policies: enrichedPolicies });
43
+ })
44
+ );
45
+
46
+ // POST /api/policies — save a new policy
47
+ router.post(
48
+ "/",
49
+ asyncHandler(async (req, res) => {
50
+ const policy = req.body;
51
+ if (!PolicyLoader.validate(policy)) {
52
+ res.status(400).json({ success: false, error: "Invalid policy schema" });
53
+ return;
54
+ }
55
+ const filePath = await PolicyLoader.save(policy, req.supabase, req.user?.id);
56
+ res.status(201).json({ success: true, filePath });
57
+ })
58
+ );
59
+
60
+ // DELETE /api/policies/:id — delete a policy by ID
61
+ router.delete(
62
+ "/:id",
63
+ asyncHandler(async (req, res) => {
64
+ const deleted = await PolicyLoader.delete(req.params["id"] as string, req.supabase, req.user?.id);
65
+ if (!deleted) {
66
+ res.status(404).json({ success: false, error: "Policy not found" });
67
+ return;
68
+ }
69
+ res.json({ success: true });
70
+ })
71
+ );
72
+
73
+ // PATCH /api/policies/:id — partial update (enabled toggle, metadata fields)
74
+ router.patch(
75
+ "/:id",
76
+ asyncHandler(async (req, res) => {
77
+ const { enabled, name, description, tags, priority } = req.body;
78
+ await PolicyLoader.patch(
79
+ req.params["id"] as string,
80
+ { enabled, name, description, tags, priority },
81
+ req.supabase,
82
+ req.user?.id
83
+ );
84
+ res.json({ success: true });
85
+ })
86
+ );
87
+
88
+ // POST /api/policies/reload — force cache invalidation
89
+ router.post(
90
+ "/reload",
91
+ asyncHandler(async (req, res) => {
92
+ PolicyLoader.invalidateCache();
93
+ const policies = await PolicyLoader.load(true, req.supabase);
94
+ res.json({ success: true, count: policies.length });
95
+ })
96
+ );
97
+
98
+ // POST /api/policies/synthesize — NL → Policy via LLM
99
+ router.post(
100
+ "/synthesize",
101
+ asyncHandler(async (req, res) => {
102
+ const { description, provider, model } = req.body;
103
+ if (!description || typeof description !== "string") {
104
+ res.status(400).json({ success: false, error: "description is required" });
105
+ return;
106
+ }
107
+ const result = await PolicyEngine.synthesizeFromNL(description, {
108
+ provider,
109
+ model,
110
+ userId: req.user?.id,
111
+ supabase: req.supabase,
112
+ });
113
+ if (!result.policy) {
114
+ res.status(503).json({ success: false, error: result.error ?? "Synthesis failed. SDK may be unavailable." });
115
+ return;
116
+ }
117
+ res.json({ success: true, policy: result.policy, warning: result.error });
118
+ })
119
+ );
120
+
121
+ export default router;
@@ -0,0 +1,90 @@
1
+ import { Router } from "express";
2
+
3
+ import { config } from "../config/index.js";
4
+ import { authMiddleware } from "../middleware/auth.js";
5
+ import { asyncHandler } from "../middleware/errorHandler.js";
6
+ import { validateBody, schemas } from "../middleware/validation.js";
7
+ import { SDKService } from "../services/SDKService.js";
8
+
9
+ const router = Router();
10
+
11
+ router.post(
12
+ "/dispatch",
13
+ authMiddleware,
14
+ validateBody(schemas.dispatchProcessing),
15
+ asyncHandler(async (req, res) => {
16
+ if (!req.user || !req.supabase) {
17
+ res.status(401).json({
18
+ success: false,
19
+ error: {
20
+ code: "AUTH_REQUIRED",
21
+ message: "Authentication required"
22
+ }
23
+ });
24
+ return;
25
+ }
26
+
27
+ const sdkAvailable = await SDKService.isAvailable();
28
+ const defaultProvider = await SDKService.getDefaultChatProvider();
29
+
30
+ const { source_type, payload } = req.body;
31
+
32
+ // Foundation-only dev path: avoid FK coupling to auth.users when auth is disabled.
33
+ if (config.security.disableAuth && !config.isProduction) {
34
+ res.status(202).json({
35
+ success: true,
36
+ job: {
37
+ id: `dev-${Date.now()}`,
38
+ user_id: req.user.id,
39
+ status: "queued",
40
+ source_type,
41
+ payload,
42
+ runtime_key: sdkAvailable ? `${defaultProvider.provider}:${defaultProvider.model}` : null,
43
+ error_message: null,
44
+ created_at: new Date().toISOString(),
45
+ updated_at: new Date().toISOString()
46
+ },
47
+ runtime: {
48
+ sdkAvailable,
49
+ provider: defaultProvider,
50
+ mode: "dev_stub"
51
+ }
52
+ });
53
+ return;
54
+ }
55
+
56
+ const { data, error } = await req.supabase
57
+ .from("processing_jobs")
58
+ .insert({
59
+ user_id: req.user.id,
60
+ status: "queued",
61
+ source_type,
62
+ payload,
63
+ runtime_key: sdkAvailable ? `${defaultProvider.provider}:${defaultProvider.model}` : null
64
+ })
65
+ .select("*")
66
+ .single();
67
+
68
+ if (error) {
69
+ res.status(500).json({
70
+ success: false,
71
+ error: {
72
+ code: "DB_INSERT_FAILED",
73
+ message: error.message
74
+ }
75
+ });
76
+ return;
77
+ }
78
+
79
+ res.status(202).json({
80
+ success: true,
81
+ job: data,
82
+ runtime: {
83
+ sdkAvailable,
84
+ provider: defaultProvider
85
+ }
86
+ });
87
+ })
88
+ );
89
+
90
+ export default router;
@@ -0,0 +1,11 @@
1
+ import { Router } from "express";
2
+ import { asyncHandler } from "../middleware/errorHandler.js";
3
+
4
+ const router = Router();
5
+
6
+ // GET /api/rules
7
+ router.get("/", asyncHandler(async (req, res) => {
8
+ res.json({ rules: [] });
9
+ }));
10
+
11
+ export default router;