@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,66 @@
|
|
|
1
|
+
import { corsHeaders } from "../_shared/cors.ts";
|
|
2
|
+
import { authenticate } from "../_shared/auth.ts";
|
|
3
|
+
|
|
4
|
+
Deno.serve(async (req: Request) => {
|
|
5
|
+
if (req.method === "OPTIONS") {
|
|
6
|
+
return new Response("ok", { headers: corsHeaders });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
const { user, client } = await authenticate(req);
|
|
11
|
+
|
|
12
|
+
if (req.method === "GET") {
|
|
13
|
+
const { data, error } = await client
|
|
14
|
+
.from("user_settings")
|
|
15
|
+
.select("*")
|
|
16
|
+
.eq("user_id", user.id)
|
|
17
|
+
.maybeSingle();
|
|
18
|
+
|
|
19
|
+
if (error) {
|
|
20
|
+
return Response.json({ error: error.message }, { status: 500, headers: corsHeaders });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return Response.json({ settings: data }, { headers: corsHeaders });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (req.method === "PATCH") {
|
|
27
|
+
const body = await req.json();
|
|
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
|
+
};
|
|
45
|
+
|
|
46
|
+
const { data, error } = await client
|
|
47
|
+
.from("user_settings")
|
|
48
|
+
.upsert({ user_id: user.id, ...payload }, { onConflict: "user_id" })
|
|
49
|
+
.select("*")
|
|
50
|
+
.single();
|
|
51
|
+
|
|
52
|
+
if (error) {
|
|
53
|
+
return Response.json({ error: error.message }, { status: 500, headers: corsHeaders });
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Response.json({ settings: data }, { headers: corsHeaders });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return Response.json({ error: "Method not allowed" }, { status: 405, headers: corsHeaders });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
return Response.json(
|
|
62
|
+
{ error: error instanceof Error ? error.message : "Unexpected error" },
|
|
63
|
+
{ status: 401, headers: corsHeaders }
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import "jsr:@supabase/functions-js/edge-runtime.d.ts";
|
|
2
|
+
import { getAdminClient } from "../_shared/supabaseAdmin.ts";
|
|
3
|
+
import { corsHeaders, createErrorResponse } from "../_shared/cors.ts";
|
|
4
|
+
|
|
5
|
+
async function createFirstUser(req: Request) {
|
|
6
|
+
try {
|
|
7
|
+
const { email, password, first_name, last_name } = await req.json();
|
|
8
|
+
console.log(`[Setup] Starting setup for ${email}`);
|
|
9
|
+
|
|
10
|
+
const supabaseAdmin = getAdminClient();
|
|
11
|
+
|
|
12
|
+
// Check if any users exist
|
|
13
|
+
const { count, error: countError } = await supabaseAdmin
|
|
14
|
+
.from("profiles")
|
|
15
|
+
.select("*", { count: "exact", head: true });
|
|
16
|
+
|
|
17
|
+
if (countError) {
|
|
18
|
+
console.error("[Setup] Error checking profiles table:", countError);
|
|
19
|
+
return createErrorResponse(500, `Database error checking profiles: ${countError.message} (code: ${countError.code})`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
console.log(`[Setup] Existing profiles count: ${count}`);
|
|
23
|
+
if (count && count > 0) {
|
|
24
|
+
return createErrorResponse(403, "First user already exists");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Create user with admin API
|
|
28
|
+
console.log("[Setup] Creating admin user...");
|
|
29
|
+
const { data, error: userError } = await supabaseAdmin.auth.admin.createUser({
|
|
30
|
+
email,
|
|
31
|
+
password,
|
|
32
|
+
email_confirm: true,
|
|
33
|
+
user_metadata: { first_name, last_name },
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
if (userError || !data?.user) {
|
|
37
|
+
console.error("[Setup] Error creating auth user:", userError);
|
|
38
|
+
return createErrorResponse(500, `Failed to create auth user: ${userError?.message || 'Unknown error'}`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
console.log(`[Setup] User created successfully: ${data.user.id}. Creating profile...`);
|
|
42
|
+
|
|
43
|
+
// Explicitly create profile as admin
|
|
44
|
+
const { error: profileError } = await supabaseAdmin
|
|
45
|
+
.from("profiles")
|
|
46
|
+
.upsert({
|
|
47
|
+
id: data.user.id,
|
|
48
|
+
email: data.user.email,
|
|
49
|
+
first_name: first_name || null,
|
|
50
|
+
last_name: last_name || null,
|
|
51
|
+
is_admin: true,
|
|
52
|
+
}, { onConflict: 'id' });
|
|
53
|
+
|
|
54
|
+
if (profileError) {
|
|
55
|
+
console.error("[Setup] Error creating profile row:", profileError);
|
|
56
|
+
return createErrorResponse(500, `User created but profile record failed: ${profileError.message} (code: ${profileError.code})`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log("[Setup] Setup completed successfully");
|
|
60
|
+
|
|
61
|
+
return new Response(
|
|
62
|
+
JSON.stringify({
|
|
63
|
+
data: {
|
|
64
|
+
id: data.user.id,
|
|
65
|
+
email: data.user.email,
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
{
|
|
69
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
70
|
+
},
|
|
71
|
+
);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error("Unexpected error in createFirstUser:", error);
|
|
74
|
+
return createErrorResponse(500, `Internal Server Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
Deno.serve(async (req: Request) => {
|
|
79
|
+
if (req.method === "OPTIONS") {
|
|
80
|
+
return new Response(null, {
|
|
81
|
+
status: 204,
|
|
82
|
+
headers: corsHeaders,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (req.method === "POST") {
|
|
87
|
+
return createFirstUser(req);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return createErrorResponse(405, "Method Not Allowed");
|
|
91
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
-- Folio foundation schema
|
|
2
|
+
|
|
3
|
+
create table if not exists public.profiles (
|
|
4
|
+
id uuid primary key references auth.users(id) on delete cascade,
|
|
5
|
+
first_name text,
|
|
6
|
+
last_name text,
|
|
7
|
+
email text,
|
|
8
|
+
avatar_url text,
|
|
9
|
+
is_admin boolean default false,
|
|
10
|
+
created_at timestamptz default now(),
|
|
11
|
+
updated_at timestamptz default now()
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
create table if not exists public.user_settings (
|
|
15
|
+
id uuid primary key default gen_random_uuid(),
|
|
16
|
+
user_id uuid not null references auth.users(id) on delete cascade unique,
|
|
17
|
+
llm_provider text,
|
|
18
|
+
llm_model text,
|
|
19
|
+
sync_interval_minutes integer not null default 5 check (sync_interval_minutes >= 1 and sync_interval_minutes <= 60),
|
|
20
|
+
created_at timestamptz default now(),
|
|
21
|
+
updated_at timestamptz default now()
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
create table if not exists public.integrations (
|
|
25
|
+
id uuid primary key default gen_random_uuid(),
|
|
26
|
+
user_id uuid not null references auth.users(id) on delete cascade,
|
|
27
|
+
provider text not null,
|
|
28
|
+
credentials jsonb not null default '{}'::jsonb,
|
|
29
|
+
is_enabled boolean not null default true,
|
|
30
|
+
created_at timestamptz default now(),
|
|
31
|
+
updated_at timestamptz default now(),
|
|
32
|
+
unique (user_id, provider)
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
create table if not exists public.processing_jobs (
|
|
36
|
+
id uuid primary key default gen_random_uuid(),
|
|
37
|
+
user_id uuid not null references auth.users(id) on delete cascade,
|
|
38
|
+
status text not null check (status in ('queued', 'running', 'completed', 'failed')),
|
|
39
|
+
source_type text not null,
|
|
40
|
+
payload jsonb not null default '{}'::jsonb,
|
|
41
|
+
runtime_key text,
|
|
42
|
+
error_message text,
|
|
43
|
+
created_at timestamptz default now(),
|
|
44
|
+
updated_at timestamptz default now()
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
create table if not exists public.system_logs (
|
|
48
|
+
id uuid primary key default gen_random_uuid(),
|
|
49
|
+
user_id uuid references auth.users(id) on delete cascade,
|
|
50
|
+
level text not null,
|
|
51
|
+
scope text not null,
|
|
52
|
+
message text not null,
|
|
53
|
+
metadata jsonb not null default '{}'::jsonb,
|
|
54
|
+
created_at timestamptz default now()
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
alter table public.profiles enable row level security;
|
|
58
|
+
alter table public.user_settings enable row level security;
|
|
59
|
+
alter table public.integrations enable row level security;
|
|
60
|
+
alter table public.processing_jobs enable row level security;
|
|
61
|
+
alter table public.system_logs enable row level security;
|
|
62
|
+
|
|
63
|
+
create policy "profiles own rows" on public.profiles
|
|
64
|
+
for all using (auth.uid() = id);
|
|
65
|
+
|
|
66
|
+
create policy "user_settings own rows" on public.user_settings
|
|
67
|
+
for all using (auth.uid() = user_id);
|
|
68
|
+
|
|
69
|
+
create policy "integrations own rows" on public.integrations
|
|
70
|
+
for all using (auth.uid() = user_id);
|
|
71
|
+
|
|
72
|
+
create policy "processing_jobs own rows" on public.processing_jobs
|
|
73
|
+
for all using (auth.uid() = user_id);
|
|
74
|
+
|
|
75
|
+
create policy "system_logs own rows" on public.system_logs
|
|
76
|
+
for all using (auth.uid() = user_id);
|
|
77
|
+
|
|
78
|
+
create index if not exists idx_user_settings_user_id on public.user_settings(user_id);
|
|
79
|
+
create index if not exists idx_integrations_user_id on public.integrations(user_id);
|
|
80
|
+
create index if not exists idx_processing_jobs_user_id on public.processing_jobs(user_id);
|
|
81
|
+
create index if not exists idx_processing_jobs_status on public.processing_jobs(status);
|
|
82
|
+
create index if not exists idx_system_logs_user_id_created_at on public.system_logs(user_id, created_at desc);
|
|
83
|
+
|
|
84
|
+
create or replace function public.update_updated_at_column()
|
|
85
|
+
returns trigger
|
|
86
|
+
language plpgsql
|
|
87
|
+
as $$
|
|
88
|
+
begin
|
|
89
|
+
new.updated_at = now();
|
|
90
|
+
return new;
|
|
91
|
+
end;
|
|
92
|
+
$$;
|
|
93
|
+
|
|
94
|
+
drop trigger if exists update_profiles_updated_at on public.profiles;
|
|
95
|
+
create trigger update_profiles_updated_at
|
|
96
|
+
before update on public.profiles
|
|
97
|
+
for each row execute function public.update_updated_at_column();
|
|
98
|
+
|
|
99
|
+
drop trigger if exists update_user_settings_updated_at on public.user_settings;
|
|
100
|
+
create trigger update_user_settings_updated_at
|
|
101
|
+
before update on public.user_settings
|
|
102
|
+
for each row execute function public.update_updated_at_column();
|
|
103
|
+
|
|
104
|
+
drop trigger if exists update_integrations_updated_at on public.integrations;
|
|
105
|
+
create trigger update_integrations_updated_at
|
|
106
|
+
before update on public.integrations
|
|
107
|
+
for each row execute function public.update_updated_at_column();
|
|
108
|
+
|
|
109
|
+
drop trigger if exists update_processing_jobs_updated_at on public.processing_jobs;
|
|
110
|
+
create trigger update_processing_jobs_updated_at
|
|
111
|
+
before update on public.processing_jobs
|
|
112
|
+
for each row execute function public.update_updated_at_column();
|
|
113
|
+
|
|
114
|
+
create or replace function public.handle_new_user()
|
|
115
|
+
returns trigger
|
|
116
|
+
language plpgsql
|
|
117
|
+
security definer
|
|
118
|
+
set search_path = public
|
|
119
|
+
as $$
|
|
120
|
+
begin
|
|
121
|
+
insert into public.profiles (id, email)
|
|
122
|
+
values (new.id, new.email)
|
|
123
|
+
on conflict (id) do nothing;
|
|
124
|
+
|
|
125
|
+
insert into public.user_settings (user_id)
|
|
126
|
+
values (new.id)
|
|
127
|
+
on conflict (user_id) do nothing;
|
|
128
|
+
|
|
129
|
+
return new;
|
|
130
|
+
end;
|
|
131
|
+
$$;
|
|
132
|
+
|
|
133
|
+
drop trigger if exists on_auth_user_created on auth.users;
|
|
134
|
+
create trigger on_auth_user_created
|
|
135
|
+
after insert on auth.users
|
|
136
|
+
for each row execute procedure public.handle_new_user();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
create or replace function public.get_latest_migration_timestamp()
|
|
2
|
+
returns text
|
|
3
|
+
language sql
|
|
4
|
+
security definer
|
|
5
|
+
set search_path = ''
|
|
6
|
+
as $$
|
|
7
|
+
select max(version) from supabase_migrations.schema_migrations;
|
|
8
|
+
$$;
|
|
9
|
+
|
|
10
|
+
grant execute on function public.get_latest_migration_timestamp() to anon, authenticated;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
-- Init-state parity with email-automator, adapted for Folio.
|
|
2
|
+
-- In Folio, user presence is represented by public.profiles.
|
|
3
|
+
|
|
4
|
+
create or replace view public.init_state
|
|
5
|
+
with (security_invoker=off)
|
|
6
|
+
as
|
|
7
|
+
select
|
|
8
|
+
count(id) as is_initialized
|
|
9
|
+
from
|
|
10
|
+
(
|
|
11
|
+
select
|
|
12
|
+
profiles.id
|
|
13
|
+
from
|
|
14
|
+
public.profiles
|
|
15
|
+
limit
|
|
16
|
+
1
|
|
17
|
+
) sub;
|
|
18
|
+
|
|
19
|
+
grant usage on schema public to anon, authenticated;
|
|
20
|
+
grant select on public.init_state to anon, authenticated;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
-- Port user creation parity from email-automator (foundation only).
|
|
2
|
+
-- This keeps Folio profile bootstrap synced with auth.users lifecycle.
|
|
3
|
+
|
|
4
|
+
create or replace function public.handle_new_auth_user()
|
|
5
|
+
returns trigger
|
|
6
|
+
language plpgsql
|
|
7
|
+
security definer
|
|
8
|
+
set search_path = 'pg_catalog', 'public'
|
|
9
|
+
as $$
|
|
10
|
+
declare
|
|
11
|
+
should_be_admin boolean;
|
|
12
|
+
begin
|
|
13
|
+
-- Serialize first-user admin assignment to avoid races.
|
|
14
|
+
perform pg_advisory_xact_lock(602240003);
|
|
15
|
+
|
|
16
|
+
select not exists (
|
|
17
|
+
select 1
|
|
18
|
+
from public.profiles p
|
|
19
|
+
where p.is_admin = true
|
|
20
|
+
)
|
|
21
|
+
into should_be_admin;
|
|
22
|
+
|
|
23
|
+
insert into public.profiles (id, first_name, last_name, email, is_admin)
|
|
24
|
+
values (
|
|
25
|
+
new.id,
|
|
26
|
+
new.raw_user_meta_data ->> 'first_name',
|
|
27
|
+
new.raw_user_meta_data ->> 'last_name',
|
|
28
|
+
new.email,
|
|
29
|
+
should_be_admin
|
|
30
|
+
)
|
|
31
|
+
on conflict (id) do update
|
|
32
|
+
set
|
|
33
|
+
first_name = coalesce(excluded.first_name, public.profiles.first_name),
|
|
34
|
+
last_name = coalesce(excluded.last_name, public.profiles.last_name),
|
|
35
|
+
email = excluded.email,
|
|
36
|
+
updated_at = now();
|
|
37
|
+
|
|
38
|
+
insert into public.user_settings (user_id)
|
|
39
|
+
values (new.id)
|
|
40
|
+
on conflict (user_id) do nothing;
|
|
41
|
+
|
|
42
|
+
return new;
|
|
43
|
+
end;
|
|
44
|
+
$$;
|
|
45
|
+
|
|
46
|
+
create or replace function public.handle_update_auth_user()
|
|
47
|
+
returns trigger
|
|
48
|
+
language plpgsql
|
|
49
|
+
security definer
|
|
50
|
+
set search_path = 'pg_catalog', 'public'
|
|
51
|
+
as $$
|
|
52
|
+
begin
|
|
53
|
+
update public.profiles
|
|
54
|
+
set
|
|
55
|
+
first_name = coalesce(new.raw_user_meta_data ->> 'first_name', first_name),
|
|
56
|
+
last_name = coalesce(new.raw_user_meta_data ->> 'last_name', last_name),
|
|
57
|
+
email = new.email,
|
|
58
|
+
updated_at = now()
|
|
59
|
+
where id = new.id;
|
|
60
|
+
|
|
61
|
+
return new;
|
|
62
|
+
end;
|
|
63
|
+
$$;
|
|
64
|
+
|
|
65
|
+
drop trigger if exists on_auth_user_created on auth.users;
|
|
66
|
+
create trigger on_auth_user_created
|
|
67
|
+
after insert on auth.users
|
|
68
|
+
for each row execute function public.handle_new_auth_user();
|
|
69
|
+
|
|
70
|
+
drop trigger if exists on_auth_user_updated on auth.users;
|
|
71
|
+
create trigger on_auth_user_updated
|
|
72
|
+
after update on auth.users
|
|
73
|
+
for each row
|
|
74
|
+
when (
|
|
75
|
+
old.email is distinct from new.email
|
|
76
|
+
or old.raw_user_meta_data is distinct from new.raw_user_meta_data
|
|
77
|
+
)
|
|
78
|
+
execute function public.handle_update_auth_user();
|
|
79
|
+
|
|
80
|
+
-- Backfill profiles for existing auth.users rows where trigger might not have run.
|
|
81
|
+
insert into public.profiles (id, first_name, last_name, email, is_admin, created_at, updated_at)
|
|
82
|
+
select
|
|
83
|
+
u.id,
|
|
84
|
+
u.raw_user_meta_data ->> 'first_name',
|
|
85
|
+
u.raw_user_meta_data ->> 'last_name',
|
|
86
|
+
u.email,
|
|
87
|
+
false,
|
|
88
|
+
coalesce(u.created_at, now()),
|
|
89
|
+
now()
|
|
90
|
+
from auth.users u
|
|
91
|
+
where not exists (
|
|
92
|
+
select 1
|
|
93
|
+
from public.profiles p
|
|
94
|
+
where p.id = u.id
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
-- Ensure exactly one bootstrap admin exists for legacy projects.
|
|
98
|
+
with admin_candidate as (
|
|
99
|
+
select p.id
|
|
100
|
+
from public.profiles p
|
|
101
|
+
left join auth.users u on u.id = p.id
|
|
102
|
+
order by coalesce(u.created_at, p.created_at), p.id
|
|
103
|
+
limit 1
|
|
104
|
+
)
|
|
105
|
+
update public.profiles p
|
|
106
|
+
set
|
|
107
|
+
is_admin = true,
|
|
108
|
+
updated_at = now()
|
|
109
|
+
where p.id in (select id from admin_candidate)
|
|
110
|
+
and not exists (
|
|
111
|
+
select 1
|
|
112
|
+
from public.profiles existing_admin
|
|
113
|
+
where existing_admin.is_admin = true
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
-- Keep profile identity fields synchronized for existing users.
|
|
117
|
+
update public.profiles p
|
|
118
|
+
set
|
|
119
|
+
first_name = coalesce(u.raw_user_meta_data ->> 'first_name', p.first_name),
|
|
120
|
+
last_name = coalesce(u.raw_user_meta_data ->> 'last_name', p.last_name),
|
|
121
|
+
email = u.email,
|
|
122
|
+
updated_at = now()
|
|
123
|
+
from auth.users u
|
|
124
|
+
where u.id = p.id
|
|
125
|
+
and (
|
|
126
|
+
p.email is distinct from u.email
|
|
127
|
+
or p.first_name is distinct from (u.raw_user_meta_data ->> 'first_name')
|
|
128
|
+
or p.last_name is distinct from (u.raw_user_meta_data ->> 'last_name')
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
-- Backfill user_settings for existing users if missing.
|
|
132
|
+
insert into public.user_settings (user_id)
|
|
133
|
+
select u.id
|
|
134
|
+
from auth.users u
|
|
135
|
+
where not exists (
|
|
136
|
+
select 1
|
|
137
|
+
from public.user_settings s
|
|
138
|
+
where s.user_id = u.id
|
|
139
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
-- Storage bucket for user avatars
|
|
2
|
+
insert into storage.buckets (id, name, public)
|
|
3
|
+
values ('avatars', 'avatars', true)
|
|
4
|
+
on conflict (id) do nothing;
|
|
5
|
+
|
|
6
|
+
-- RLS policies for avatars bucket
|
|
7
|
+
create policy "Avatar upload" on storage.objects
|
|
8
|
+
for insert with check (
|
|
9
|
+
bucket_id = 'avatars'
|
|
10
|
+
and (storage.foldername(name))[1] = auth.uid()::text
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
create policy "Avatar update" on storage.objects
|
|
14
|
+
for update with check (
|
|
15
|
+
bucket_id = 'avatars'
|
|
16
|
+
and (storage.foldername(name))[1] = auth.uid()::text
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
create policy "Avatar delete" on storage.objects
|
|
20
|
+
for delete using (
|
|
21
|
+
bucket_id = 'avatars'
|
|
22
|
+
and (storage.foldername(name))[1] = auth.uid()::text
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
create policy "Avatar public access" on storage.objects
|
|
26
|
+
for select using (bucket_id = 'avatars');
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
-- Migration: Add TTS and Embedding settings to user_settings
|
|
2
|
+
-- Created: 2026-02-24
|
|
3
|
+
|
|
4
|
+
-- Add TTS settings
|
|
5
|
+
ALTER TABLE public.user_settings
|
|
6
|
+
ADD COLUMN IF NOT EXISTS tts_auto_play BOOLEAN DEFAULT false,
|
|
7
|
+
ADD COLUMN IF NOT EXISTS tts_provider TEXT DEFAULT 'piper_local',
|
|
8
|
+
ADD COLUMN IF NOT EXISTS tts_voice TEXT DEFAULT NULL,
|
|
9
|
+
ADD COLUMN IF NOT EXISTS tts_speed NUMERIC DEFAULT 1.0,
|
|
10
|
+
ADD COLUMN IF NOT EXISTS tts_quality INTEGER DEFAULT 10;
|
|
11
|
+
|
|
12
|
+
-- Add Embedding settings
|
|
13
|
+
ALTER TABLE public.user_settings
|
|
14
|
+
ADD COLUMN IF NOT EXISTS embedding_provider TEXT DEFAULT 'realtimexai',
|
|
15
|
+
ADD COLUMN IF NOT EXISTS embedding_model TEXT DEFAULT 'text-embedding-3-small';
|
|
16
|
+
|
|
17
|
+
-- Add comments for documentation
|
|
18
|
+
COMMENT ON COLUMN public.user_settings.tts_auto_play IS 'Automatically read AI responses aloud using text-to-speech';
|
|
19
|
+
COMMENT ON COLUMN public.user_settings.tts_provider IS 'TTS provider (piper_local, supertonic_local, etc.)';
|
|
20
|
+
COMMENT ON COLUMN public.user_settings.tts_voice IS 'Voice ID specific to the selected provider';
|
|
21
|
+
COMMENT ON COLUMN public.user_settings.tts_speed IS 'Speech speed (0.5x to 2.0x)';
|
|
22
|
+
COMMENT ON COLUMN public.user_settings.tts_quality IS 'Audio quality/bitrate (1-20, higher = better quality)';
|
|
23
|
+
COMMENT ON COLUMN public.user_settings.embedding_provider IS 'Default embedding provider for RAG and search';
|
|
24
|
+
COMMENT ON COLUMN public.user_settings.embedding_model IS 'Default embedding model name';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
-- Create policies table for user-managed FPE policies
|
|
2
|
+
CREATE TABLE IF NOT EXISTS policies (
|
|
3
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4
|
+
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
5
|
+
policy_id TEXT NOT NULL, -- the metadata.id from the YAML (e.g. "tesla-invoice-handler")
|
|
6
|
+
metadata JSONB NOT NULL,
|
|
7
|
+
spec JSONB NOT NULL,
|
|
8
|
+
enabled BOOLEAN NOT NULL DEFAULT true,
|
|
9
|
+
priority INTEGER NOT NULL DEFAULT 100,
|
|
10
|
+
api_version TEXT NOT NULL DEFAULT 'folio/v1',
|
|
11
|
+
kind TEXT NOT NULL DEFAULT 'Policy',
|
|
12
|
+
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
13
|
+
updated_at TIMESTAMPTZ DEFAULT NOW(),
|
|
14
|
+
UNIQUE(user_id, policy_id)
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
-- Enable RLS
|
|
18
|
+
ALTER TABLE policies ENABLE ROW LEVEL SECURITY;
|
|
19
|
+
|
|
20
|
+
-- Users can only manage their own policies
|
|
21
|
+
CREATE POLICY "Users can read own policies"
|
|
22
|
+
ON policies FOR SELECT
|
|
23
|
+
USING (auth.uid() = user_id);
|
|
24
|
+
|
|
25
|
+
CREATE POLICY "Users can insert own policies"
|
|
26
|
+
ON policies FOR INSERT
|
|
27
|
+
WITH CHECK (auth.uid() = user_id);
|
|
28
|
+
|
|
29
|
+
CREATE POLICY "Users can update own policies"
|
|
30
|
+
ON policies FOR UPDATE
|
|
31
|
+
USING (auth.uid() = user_id);
|
|
32
|
+
|
|
33
|
+
CREATE POLICY "Users can delete own policies"
|
|
34
|
+
ON policies FOR DELETE
|
|
35
|
+
USING (auth.uid() = user_id);
|
|
36
|
+
|
|
37
|
+
-- Auto-update updated_at
|
|
38
|
+
CREATE OR REPLACE FUNCTION update_policies_updated_at()
|
|
39
|
+
RETURNS TRIGGER AS $$
|
|
40
|
+
BEGIN
|
|
41
|
+
NEW.updated_at = NOW();
|
|
42
|
+
RETURN NEW;
|
|
43
|
+
END;
|
|
44
|
+
$$ LANGUAGE plpgsql;
|
|
45
|
+
|
|
46
|
+
CREATE TRIGGER policies_updated_at
|
|
47
|
+
BEFORE UPDATE ON policies
|
|
48
|
+
FOR EACH ROW EXECUTE FUNCTION update_policies_updated_at();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
-- Fix get_latest_migration_timestamp() to exclude sentinel/test migrations
|
|
2
|
+
-- Migrations with timestamps >= 29990000000000 are development-only sentinels
|
|
3
|
+
-- and should not influence the migration version comparison logic.
|
|
4
|
+
CREATE OR REPLACE FUNCTION get_latest_migration_timestamp()
|
|
5
|
+
RETURNS text AS $$
|
|
6
|
+
SELECT max(version)::text
|
|
7
|
+
FROM supabase_migrations.schema_migrations
|
|
8
|
+
WHERE version < '29990000000000';
|
|
9
|
+
$$ LANGUAGE sql SECURITY DEFINER;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
-- Ingestion log table: tracks every document processed through Folio's Policy Engine
|
|
2
|
+
CREATE TABLE IF NOT EXISTS ingestions (
|
|
3
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
4
|
+
user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE,
|
|
5
|
+
source text NOT NULL DEFAULT 'upload', -- 'upload' | 'dropzone' | 'email' | 'url'
|
|
6
|
+
filename text NOT NULL,
|
|
7
|
+
mime_type text,
|
|
8
|
+
file_size bigint,
|
|
9
|
+
status text NOT NULL DEFAULT 'pending', -- 'pending' | 'processing' | 'matched' | 'no_match' | 'error'
|
|
10
|
+
policy_id text, -- matched policy id (nullable)
|
|
11
|
+
policy_name text, -- denormalised for display
|
|
12
|
+
extracted jsonb DEFAULT '{}'::jsonb, -- key/value pairs extracted by FPE
|
|
13
|
+
actions_taken jsonb DEFAULT '[]'::jsonb, -- list of actions executed
|
|
14
|
+
error_message text,
|
|
15
|
+
storage_path text, -- supabase storage path (if file stored)
|
|
16
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
17
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
-- Index for per-user list queries
|
|
21
|
+
CREATE INDEX IF NOT EXISTS ingestions_user_id_idx ON ingestions(user_id, created_at DESC);
|
|
22
|
+
|
|
23
|
+
-- RLS
|
|
24
|
+
ALTER TABLE ingestions ENABLE ROW LEVEL SECURITY;
|
|
25
|
+
|
|
26
|
+
CREATE POLICY "Users can manage their own ingestions"
|
|
27
|
+
ON ingestions FOR ALL
|
|
28
|
+
USING (auth.uid() = user_id)
|
|
29
|
+
WITH CHECK (auth.uid() = user_id);
|
|
30
|
+
|
|
31
|
+
-- updated_at trigger
|
|
32
|
+
CREATE OR REPLACE FUNCTION update_ingestions_updated_at()
|
|
33
|
+
RETURNS TRIGGER AS $$
|
|
34
|
+
BEGIN
|
|
35
|
+
NEW.updated_at = now();
|
|
36
|
+
RETURN NEW;
|
|
37
|
+
END;
|
|
38
|
+
$$ LANGUAGE plpgsql;
|
|
39
|
+
|
|
40
|
+
CREATE TRIGGER ingestions_updated_at
|
|
41
|
+
BEFORE UPDATE ON ingestions
|
|
42
|
+
FOR EACH ROW EXECUTE FUNCTION update_ingestions_updated_at();
|