Skip to content

Commit 4e0bf34

Browse files
committed
add Installer.run() API for simplified Lovable integration
1 parent 7caca1d commit 4e0bf34

File tree

9 files changed

+893
-295
lines changed

9 files changed

+893
-295
lines changed

pkgs/edge-worker/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@ export { FlowWorkerLifecycle } from './flow/FlowWorkerLifecycle.js';
99
// Export ControlPlane for HTTP-based flow compilation
1010
export { ControlPlane } from './control-plane/index.js';
1111

12+
// Export Installer for no-CLI platforms (e.g., Lovable)
13+
export { Installer } from './installer/index.js';
14+
1215
// Export platform adapters
1316
export * from './platform/index.js';
1417

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { createInstallerHandler } from './server.ts';
2+
3+
export const Installer = {
4+
run: (token: string) => {
5+
const handler = createInstallerHandler(token);
6+
Deno.serve({}, handler);
7+
},
8+
};
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
import type { InstallerResult, StepResult } from './types.ts';
2+
import postgres from 'postgres';
3+
import { MigrationRunner } from '../control-plane/migrations/index.ts';
4+
import { extractProjectId } from '../control-plane/server.ts';
5+
6+
// Dependency injection for testability
7+
export interface InstallerDeps {
8+
getEnv: (key: string) => string | undefined;
9+
}
10+
11+
const defaultDeps: InstallerDeps = {
12+
getEnv: (key) => Deno.env.get(key),
13+
};
14+
15+
export function createInstallerHandler(
16+
expectedToken: string,
17+
deps: InstallerDeps = defaultDeps
18+
): (req: Request) => Promise<Response> {
19+
return async (req: Request) => {
20+
// Validate token from query params first (fail fast)
21+
const url = new URL(req.url);
22+
const token = url.searchParams.get('token');
23+
24+
if (token !== expectedToken) {
25+
return jsonResponse(
26+
{
27+
success: false,
28+
message:
29+
'Invalid or missing token. Use the exact URL from your Lovable prompt.',
30+
},
31+
401
32+
);
33+
}
34+
35+
// Read env vars inside handler (not at module level)
36+
const supabaseUrl = deps.getEnv('SUPABASE_URL');
37+
const serviceRoleKey = deps.getEnv('SUPABASE_SERVICE_ROLE_KEY');
38+
const dbUrl = deps.getEnv('SUPABASE_DB_URL');
39+
40+
if (!supabaseUrl || !serviceRoleKey) {
41+
return jsonResponse(
42+
{
43+
success: false,
44+
message: 'Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY',
45+
},
46+
500
47+
);
48+
}
49+
50+
if (!dbUrl) {
51+
return jsonResponse(
52+
{
53+
success: false,
54+
message: 'Missing SUPABASE_DB_URL',
55+
},
56+
500
57+
);
58+
}
59+
60+
console.log('pgflow installer starting...');
61+
62+
// Create database connection
63+
const sql = postgres(dbUrl, { prepare: false });
64+
65+
let secrets: StepResult;
66+
let migrations: StepResult;
67+
68+
try {
69+
// Step 1: Configure vault secrets
70+
console.log('Configuring vault secrets...');
71+
secrets = await configureSecrets(sql, supabaseUrl, serviceRoleKey);
72+
73+
if (!secrets.success) {
74+
const result: InstallerResult = {
75+
success: false,
76+
secrets,
77+
migrations: {
78+
success: false,
79+
status: 0,
80+
error: 'Skipped - secrets failed',
81+
},
82+
message: 'Failed to configure vault secrets.',
83+
};
84+
return jsonResponse(result, 500);
85+
}
86+
87+
// Step 2: Run migrations
88+
console.log('Running migrations...');
89+
migrations = await runMigrations(sql);
90+
91+
const result: InstallerResult = {
92+
success: secrets.success && migrations.success,
93+
secrets,
94+
migrations,
95+
message: migrations.success
96+
? 'pgflow installed successfully! Vault secrets configured and migrations applied.'
97+
: 'Secrets configured but migrations failed. Check the error details.',
98+
};
99+
100+
console.log('Installer complete:', result.message);
101+
return jsonResponse(result, result.success ? 200 : 500);
102+
} finally {
103+
await sql.end();
104+
}
105+
};
106+
}
107+
108+
/**
109+
* Configure vault secrets for pgflow
110+
*/
111+
async function configureSecrets(
112+
sql: postgres.Sql,
113+
supabaseUrl: string,
114+
serviceRoleKey: string
115+
): Promise<StepResult> {
116+
try {
117+
const projectId = extractProjectId(supabaseUrl);
118+
if (!projectId) {
119+
return {
120+
success: false,
121+
status: 500,
122+
error: 'Could not extract project ID from SUPABASE_URL',
123+
};
124+
}
125+
126+
// Upsert secrets (delete + create pattern) in single transaction
127+
await sql.begin(async (tx) => {
128+
await tx`DELETE FROM vault.secrets WHERE name = 'supabase_project_id'`;
129+
await tx`SELECT vault.create_secret(${projectId}, 'supabase_project_id')`;
130+
131+
await tx`DELETE FROM vault.secrets WHERE name = 'supabase_service_role_key'`;
132+
await tx`SELECT vault.create_secret(${serviceRoleKey}, 'supabase_service_role_key')`;
133+
});
134+
135+
return {
136+
success: true,
137+
status: 200,
138+
data: { configured: ['supabase_project_id', 'supabase_service_role_key'] },
139+
};
140+
} catch (error) {
141+
return {
142+
success: false,
143+
status: 500,
144+
error: error instanceof Error ? error.message : 'Unknown error',
145+
};
146+
}
147+
}
148+
149+
/**
150+
* Run pending migrations
151+
*/
152+
async function runMigrations(sql: postgres.Sql): Promise<StepResult> {
153+
try {
154+
const runner = new MigrationRunner(sql);
155+
const result = await runner.up();
156+
157+
return {
158+
success: result.success,
159+
status: result.success ? 200 : 500,
160+
data: result,
161+
};
162+
} catch (error) {
163+
return {
164+
success: false,
165+
status: 500,
166+
error: error instanceof Error ? error.message : 'Unknown error',
167+
};
168+
}
169+
}
170+
171+
function jsonResponse(data: unknown, status: number): Response {
172+
return new Response(JSON.stringify(data, null, 2), {
173+
status,
174+
headers: { 'Content-Type': 'application/json' },
175+
});
176+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export interface StepResult {
2+
success: boolean;
3+
status: number;
4+
data?: unknown;
5+
error?: string;
6+
}
7+
8+
export interface InstallerResult {
9+
success: boolean;
10+
secrets: StepResult;
11+
migrations: StepResult;
12+
message: string;
13+
}

0 commit comments

Comments
 (0)