workspace.ts198 lines · main
1/**
2 * briven-engine dashboard workspace — projects the operator can manage,
3 * with Auth on/off based on be_tenants (Doltgres).
4 */
5
6import { listProjectsForUser } from '../projects.js';
7import { getEnginePool } from './db.js';
8import { isAuthCoreInitialized } from './engine.js';
9import { ensureBrivenEngineTenant } from './multitenancy.js';
10import { getBrivenEngineProjectConfig } from './project-config.js';
11import { mapProjectToAuthCore } from './project-map.js';
12
13export type BrivenEngineWorkspaceProject = {
14 id: string;
15 slug: string;
16 name: string;
17 authEnabled: boolean;
18 tenantId: string | null;
19 providers: {
20 emailPassword: boolean;
21 magicLink: boolean;
22 emailOtp: boolean;
23 passkey: boolean;
24 } | null;
25 error?: boolean;
26};
27
28/**
29 * Enable Auth for a project = create briven-engine tenant island on Doltgres.
30 * Idempotent.
31 */
32export async function enableBrivenEngineAuth(projectId: string): Promise<{
33 ok: boolean;
34 engine: 'briven-engine';
35 projectId: string;
36 tenantId: string;
37 authEnabled: boolean;
38 created: boolean;
39 message?: string;
40 storage: 'doltgres';
41}> {
42 const result = await ensureBrivenEngineTenant(projectId);
43 return {
44 ok: result.ok,
45 engine: 'briven-engine',
46 projectId: result.projectId,
47 tenantId: result.tenantId,
48 authEnabled: result.ok,
49 created: result.created,
50 message: result.message,
51 storage: 'doltgres',
52 };
53}
54
55/**
56 * Whether Auth is on for a project (tenant row exists).
57 * Prefer exact tenant_id match (always reliable on Doltgres).
58 */
59export async function isBrivenEngineAuthEnabled(
60 projectId: string,
61): Promise<boolean> {
62 if (!isAuthCoreInitialized()) return false;
63 try {
64 const map = mapProjectToAuthCore(projectId);
65 const pool = getEnginePool();
66 const res = await pool.query(
67 `SELECT 1 FROM be_tenants
68 WHERE tenant_id = $1 OR project_id = $2
69 LIMIT 1`,
70 [map.tenantId, map.projectId],
71 );
72 return Boolean(res.rowCount && res.rowCount > 0);
73 } catch {
74 return false;
75 }
76}
77
78function markEnabled(
79 set: Set<string>,
80 projectId: string | null | undefined,
81): void {
82 if (!projectId) return;
83 set.add(projectId);
84 set.add(projectId.toLowerCase());
85}
86
87/**
88 * All projects the user can see + Auth on/off from briven-engine.
89 */
90export async function listBrivenEngineWorkspace(
91 userId: string,
92): Promise<{ engine: 'briven-engine'; projects: BrivenEngineWorkspaceProject[] }> {
93 const projects = await listProjectsForUser(userId);
94
95 // Build maps first — Auth on = be_tenants row for that project's tenant_id.
96 const maps = projects
97 .map((p) => {
98 try {
99 return mapProjectToAuthCore(p.id);
100 } catch {
101 return null;
102 }
103 })
104 .filter((m): m is NonNullable<typeof m> => m != null);
105
106 const tenantToProject = new Map(maps.map((m) => [m.tenantId, m.projectId]));
107 let enabledProjectIds = new Set<string>();
108
109 if (isAuthCoreInitialized() && maps.length > 0) {
110 try {
111 const pool = getEnginePool();
112 // Load all tenants (small table) — avoids IN-clause / lower() quirks on Doltgres.
113 const res = await pool.query(
114 `SELECT project_id, tenant_id FROM be_tenants`,
115 );
116 for (const row of res.rows as Array<{
117 project_id: string;
118 tenant_id: string;
119 }>) {
120 markEnabled(enabledProjectIds, row.project_id);
121 const viaTenant = tenantToProject.get(row.tenant_id);
122 markEnabled(enabledProjectIds, viaTenant);
123 // Also match tenant_id → project when project_id column is stale/mismatched
124 if (row.tenant_id.startsWith('proj-')) {
125 const fromTenant = tenantToProject.get(row.tenant_id);
126 markEnabled(enabledProjectIds, fromTenant);
127 }
128 }
129 } catch {
130 enabledProjectIds = new Set();
131 }
132 }
133
134 const rows: BrivenEngineWorkspaceProject[] = await Promise.all(
135 projects.map(async (p) => {
136 const name = (p as { name?: string | null }).name?.trim() || p.slug;
137 let tenantId: string | null = null;
138 try {
139 tenantId = mapProjectToAuthCore(p.id).tenantId;
140 } catch {
141 tenantId = null;
142 }
143
144 let authEnabled =
145 enabledProjectIds.has(p.id) ||
146 enabledProjectIds.has(p.id.toLowerCase());
147
148 // Direct probe when batch missed (should be rare).
149 if (!authEnabled && isAuthCoreInitialized()) {
150 authEnabled = await isBrivenEngineAuthEnabled(p.id);
151 }
152
153 if (!authEnabled) {
154 return {
155 id: p.id,
156 slug: p.slug,
157 name,
158 authEnabled: false,
159 // Do not show mapped tenant as if Auth were on
160 tenantId: null,
161 providers: null,
162 };
163 }
164 try {
165 const config = await getBrivenEngineProjectConfig(p.id);
166 return {
167 id: p.id,
168 slug: p.slug,
169 name,
170 authEnabled: true,
171 tenantId: config.tenantId,
172 providers: {
173 emailPassword: config.recipes.emailPassword,
174 magicLink: config.recipes.passwordless,
175 emailOtp: config.recipes.passwordless,
176 passkey: config.recipes.webauthn,
177 },
178 };
179 } catch {
180 return {
181 id: p.id,
182 slug: p.slug,
183 name,
184 authEnabled: true,
185 tenantId,
186 providers: {
187 emailPassword: true,
188 magicLink: true,
189 emailOtp: true,
190 passkey: true,
191 },
192 };
193 }
194 }),
195 );
196
197 return { engine: 'briven-engine', projects: rows };
198}