Skip to content

Conversation

@jumski
Copy link
Contributor

@jumski jumski commented Dec 19, 2025

Add Installer Module for No-CLI Platforms

This PR introduces a new Installer module to support pgflow installation on platforms without CLI access, specifically targeting Lovable. The installer provides a secure way to run database migrations and configure vault secrets through a temporary Edge Function.

Key features:

  • Creates a token-protected installer endpoint that can be invoked via HTTP
  • Implements a secure authentication mechanism with random function names and tokens
  • Provides a clean API for running migrations and configuring vault secrets
  • Includes comprehensive error handling and detailed response formatting

The PR also adds a new InstallerPrompt component for the website that generates:

  • Random function suffix (8 chars) to make the function name unpredictable
  • Random token (UUID) to authorize requests

This double security layer ensures the temporary installer function cannot be discovered or invoked by unauthorized parties.

The implementation is thoroughly tested with unit tests covering various scenarios including authentication, error handling, and successful installation flows.

@changeset-bot
Copy link

changeset-bot bot commented Dec 19, 2025

⚠️ No Changeset found

Latest commit: b7390c4

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor Author

jumski commented Dec 19, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@nx-cloud
Copy link

nx-cloud bot commented Dec 19, 2025

View your CI Pipeline Execution ↗ for commit b7390c4

Command Status Duration Result
nx run edge-worker:test:integration ✅ Succeeded 3m 50s View ↗
nx run client:e2e ✅ Succeeded 2m 49s View ↗
nx run edge-worker:e2e ✅ Succeeded 37s View ↗
nx affected -t verify-exports --base=origin/mai... ✅ Succeeded 3s View ↗
nx run cli:e2e ✅ Succeeded 3s View ↗
nx affected -t build --configuration=production... ✅ Succeeded 3s View ↗
nx affected -t lint typecheck test --parallel -... ✅ Succeeded 39s View ↗
nx run core:pgtap ✅ Succeeded 1s View ↗

☁️ Nx Cloud last updated this comment at 2025-12-22 11:15:19 UTC

@jumski jumski force-pushed the 12-19-add_installer.run_api_for_simplified_lovable_integration branch from 523aa71 to 6ab1501 Compare December 19, 2025 13:05
@jumski jumski force-pushed the 12-11-add_migration_installer_api_endpoints_for_no-cli_platforms branch from 4b6d099 to 7caca1d Compare December 22, 2025 10:31
@jumski jumski force-pushed the 12-19-add_installer.run_api_for_simplified_lovable_integration branch from 6ab1501 to 4e0bf34 Compare December 22, 2025 10:31
Comment on lines 1 to 118
import { assertEquals, assertMatch } from '@std/assert';
import {
createInstallerHandler,
type InstallerDeps,
} from '../../../src/installer/server.ts';

// Helper to create mock dependencies
function createMockDeps(overrides?: Partial<InstallerDeps>): InstallerDeps {
return {
fetch: () => Promise.resolve(new Response('{}', { status: 200 })),
getEnv: (key: string) =>
({
SUPABASE_URL: 'https://test-project.supabase.co',
SUPABASE_SERVICE_ROLE_KEY: 'test-service-role-key',
})[key],
...overrides,
};
}

// Helper to create a request with optional token
function createRequest(token?: string): Request {
const url = token
? `http://localhost/pgflow-installer?token=${token}`
: 'http://localhost/pgflow-installer';
return new Request(url);
}

// ============================================================
// Token validation tests
// ============================================================

Deno.test('Installer Handler - returns 401 when token missing', async () => {
const deps = createMockDeps();
const handler = createInstallerHandler('expected-token', deps);

const request = createRequest(); // no token
const response = await handler(request);

assertEquals(response.status, 401);
const data = await response.json();
assertEquals(data.success, false);
assertMatch(data.message, /Invalid or missing token/);
});

Deno.test('Installer Handler - returns 401 when token incorrect', async () => {
const deps = createMockDeps();
const handler = createInstallerHandler('expected-token', deps);

const request = createRequest('wrong-token');
const response = await handler(request);

assertEquals(response.status, 401);
const data = await response.json();
assertEquals(data.success, false);
assertMatch(data.message, /Invalid or missing token/);
});

// ============================================================
// Environment variable validation tests
// ============================================================

Deno.test('Installer Handler - returns 500 when SUPABASE_URL missing', async () => {
const deps = createMockDeps({
getEnv: (key: string) =>
({
SUPABASE_SERVICE_ROLE_KEY: 'test-key',
// SUPABASE_URL is undefined
})[key],
});
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
const response = await handler(request);

assertEquals(response.status, 500);
const data = await response.json();
assertEquals(data.success, false);
assertMatch(data.message, /Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY/);
});

Deno.test('Installer Handler - returns 500 when SUPABASE_SERVICE_ROLE_KEY missing', async () => {
const deps = createMockDeps({
getEnv: (key: string) =>
({
SUPABASE_URL: 'https://test.supabase.co',
// SUPABASE_SERVICE_ROLE_KEY is undefined
})[key],
});
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
const response = await handler(request);

assertEquals(response.status, 500);
const data = await response.json();
assertEquals(data.success, false);
assertMatch(data.message, /Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY/);
});

// ============================================================
// Control Plane call tests - /secrets/configure
// ============================================================

Deno.test('Installer Handler - returns failure when /secrets/configure fails', async () => {
const mockFetch = (url: string) => {
if (url.includes('/secrets/configure')) {
return Promise.resolve(
new Response(
JSON.stringify({ error: 'Vault error' }),
{ status: 500 }
)
);
}
return Promise.resolve(new Response('{}', { status: 200 }));
};

const deps = createMockDeps({ fetch: mockFetch });
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
const response = await handler(request);

assertEquals(response.status, 500);
const data = await response.json();
assertEquals(data.success, false);
assertEquals(data.secrets.success, false);
assertEquals(data.secrets.status, 500);
assertMatch(data.message, /Failed to configure vault secrets/);
});

Deno.test('Installer Handler - skips migrations if secrets fail', async () => {
let migrationsCallMade = false;

const mockFetch = (url: string) => {
if (url.includes('/secrets/configure')) {
return Promise.resolve(
new Response(
JSON.stringify({ error: 'Vault error' }),
{ status: 500 }
)
);
}
if (url.includes('/migrations/up')) {
migrationsCallMade = true;
}
return Promise.resolve(new Response('{}', { status: 200 }));
};

const deps = createMockDeps({ fetch: mockFetch });
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
await handler(request);

assertEquals(migrationsCallMade, false, 'migrations should not be called when secrets fail');
});

// ============================================================
// Control Plane call tests - /migrations/up
// ============================================================

Deno.test('Installer Handler - returns failure when /migrations/up fails', async () => {
const mockFetch = (url: string) => {
if (url.includes('/secrets/configure')) {
return Promise.resolve(
new Response(
JSON.stringify({ success: true }),
{ status: 200 }
)
);
}
if (url.includes('/migrations/up')) {
return Promise.resolve(
new Response(
JSON.stringify({ error: 'Migration error' }),
{ status: 500 }
)
);
}
return Promise.resolve(new Response('{}', { status: 200 }));
};

const deps = createMockDeps({ fetch: mockFetch });
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
const response = await handler(request);

assertEquals(response.status, 500);
const data = await response.json();
assertEquals(data.success, false);
assertEquals(data.secrets.success, true);
assertEquals(data.migrations.success, false);
assertEquals(data.migrations.status, 500);
assertMatch(data.message, /Secrets configured but migrations failed/);
});

// ============================================================
// Success case
// ============================================================

Deno.test('Installer Handler - returns success when both calls succeed', async () => {
const mockFetch = (url: string) => {
if (url.includes('/secrets/configure')) {
return Promise.resolve(
new Response(
JSON.stringify({ success: true, message: 'Secrets stored' }),
{ status: 200 }
)
);
}
if (url.includes('/migrations/up')) {
return Promise.resolve(
new Response(
JSON.stringify({ success: true, migrations: ['001_init'] }),
{ status: 200 }
)
);
}
return Promise.resolve(new Response('{}', { status: 200 }));
};

const deps = createMockDeps({ fetch: mockFetch });
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
const response = await handler(request);

assertEquals(response.status, 200);
const data = await response.json();
assertEquals(data.success, true);
assertEquals(data.secrets.success, true);
assertEquals(data.migrations.success, true);
assertMatch(data.message, /pgflow installed successfully/);
});

// ============================================================
// Control Plane endpoint construction tests
// ============================================================

Deno.test('Installer Handler - calls correct Control Plane endpoints', async () => {
const calledUrls: string[] = [];

const mockFetch = (url: string) => {
calledUrls.push(url);
return Promise.resolve(
new Response(JSON.stringify({ success: true }), { status: 200 })
);
};

const deps = createMockDeps({ fetch: mockFetch });
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
await handler(request);

assertEquals(calledUrls.length, 2);
assertEquals(
calledUrls[0],
'https://test-project.supabase.co/functions/v1/pgflow/secrets/configure'
);
assertEquals(
calledUrls[1],
'https://test-project.supabase.co/functions/v1/pgflow/migrations/up'
);
});

Deno.test('Installer Handler - sends Authorization header to Control Plane', async () => {
let capturedHeaders: Headers | undefined;

const mockFetch = (_url: string, init?: RequestInit) => {
capturedHeaders = new Headers(init?.headers);
return Promise.resolve(
new Response(JSON.stringify({ success: true }), { status: 200 })
);
};

const deps = createMockDeps({ fetch: mockFetch });
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
await handler(request);

assertEquals(
capturedHeaders?.get('Authorization'),
'Bearer test-service-role-key'
);
assertEquals(capturedHeaders?.get('Content-Type'), 'application/json');
});

// ============================================================
// Network error handling tests
// ============================================================

Deno.test('Installer Handler - handles network error on /secrets/configure', async () => {
const mockFetch = (url: string) => {
if (url.includes('/secrets/configure')) {
return Promise.reject(new Error('Network connection failed'));
}
return Promise.resolve(new Response('{}', { status: 200 }));
};

const deps = createMockDeps({ fetch: mockFetch });
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
const response = await handler(request);

assertEquals(response.status, 500);
const data = await response.json();
assertEquals(data.success, false);
assertEquals(data.secrets.success, false);
assertEquals(data.secrets.status, 0);
assertMatch(data.secrets.error!, /Network connection failed/);
});

Deno.test('Installer Handler - handles network error on /migrations/up', async () => {
const mockFetch = (url: string) => {
if (url.includes('/secrets/configure')) {
return Promise.resolve(
new Response(JSON.stringify({ success: true }), { status: 200 })
);
}
if (url.includes('/migrations/up')) {
return Promise.reject(new Error('Connection timeout'));
}
return Promise.resolve(new Response('{}', { status: 200 }));
};

const deps = createMockDeps({ fetch: mockFetch });
const handler = createInstallerHandler('valid-token', deps);

const request = createRequest('valid-token');
const response = await handler(request);

assertEquals(response.status, 500);
const data = await response.json();
assertEquals(data.success, false);
assertEquals(data.secrets.success, true);
assertEquals(data.migrations.success, false);
assertEquals(data.migrations.status, 0);
assertMatch(data.migrations.error!, /Connection timeout/);
});
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Critical: Tests mock the wrong implementation entirely

The tests mock a fetch function and test HTTP calls to Control Plane endpoints (/secrets/configure, /migrations/up), but the actual implementation in server.ts doesn't use HTTP at all.

The real implementation:

  • Directly calls configureSecrets(sql, supabaseUrl, serviceRoleKey) which executes SQL queries
  • Directly calls runMigrations(sql) which uses MigrationRunner
  • Never makes any HTTP requests

The tests:

  • Mock fetch (not used by real code)
  • Test URLs like https://test-project.supabase.co/functions/v1/pgflow/secrets/configure
  • Validate Authorization headers for HTTP calls that never happen

These tests will pass but provide zero coverage of the actual functionality. The real code could be completely broken and these tests would still pass.

Fix: Rewrite tests to:

  1. Mock the postgres client instead of fetch
  2. Test SQL query execution via configureSecrets() and runMigrations()
  3. Remove all HTTP endpoint mocking and validation

Spotted by Graphite Agent

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

@jumski jumski force-pushed the 12-19-add_installer.run_api_for_simplified_lovable_integration branch from 4e0bf34 to 4903940 Compare December 22, 2025 10:56
@jumski jumski force-pushed the 12-19-add_installer.run_api_for_simplified_lovable_integration branch from 4903940 to b7390c4 Compare December 22, 2025 11:08
@github-actions
Copy link
Contributor

🔍 Preview Deployment: Website

Deployment successful!

🔗 Preview URL: https://pr-551.pgflow.pages.dev

📝 Details:

  • Branch: 12-19-add_installer.run_api_for_simplified_lovable_integration
  • Commit: 83cf183655308ae8746a7b8d9beb7f8bba3d539f
  • View Logs

_Last updated: _

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants