Skip to content
Open
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ builders.
- [ ] Leave comments
- [ ] Advanced AI capabilities
- [x] Queue multiple messages at once
- [x] Cynthia Design Auditor - AI-powered UDEC scoring across 13 design axes
- [ ] Use Images as references and as assets in a project
- [ ] Setup and use MCPs in projects
- [ ] Allow Onlook to use itself as a toolcall for branch creation and iteration
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- PHASE 3: Add Audit Fields to Build Sessions
-- Created: 2025-12-19
-- Purpose: Wire build sessions to real Cynthia audits

-- ============================================================
-- ENUMS
-- ============================================================

-- Build session audit status enum
CREATE TYPE build_session_audit_status AS ENUM (
'pending',
'running',
'completed',
'failed'
);

-- ============================================================
-- ALTER TABLES
-- ============================================================

-- Add audit fields to build_sessions
ALTER TABLE build_sessions
ADD COLUMN audit_id UUID REFERENCES cynthia_audits(id) ON DELETE SET NULL ON UPDATE CASCADE,
ADD COLUMN audit_status build_session_audit_status DEFAULT 'pending';
Comment on lines +21 to +24

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Migration references missing cynthia_audits table

This migration adds audit_id with a foreign key to cynthia_audits, but no migration creates that table (rg over apps/backend/supabase/migrations finds only this reference), so applying this script will fail with a missing relation error and block the migration stack. The table or its creation migration needs to be added before introducing the FK.

Useful? React with 👍 / 👎.


-- ============================================================
-- INDEXES
-- ============================================================

-- Index on build_sessions.audit_id for audit lookups
CREATE INDEX idx_build_sessions_audit_id ON build_sessions(audit_id);

-- Index on build_sessions.audit_status for filtering by status
CREATE INDEX idx_build_sessions_audit_status ON build_sessions(audit_status);

-- ============================================================
-- COMMENTS
-- ============================================================

COMMENT ON COLUMN build_sessions.audit_id IS 'Phase 3: Links to real Cynthia audit (nullable until audit completes)';
COMMENT ON COLUMN build_sessions.audit_status IS 'Phase 3: Tracks audit processing state (pending → running → completed/failed)';
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
-- PHASE 2: Build Sessions & Preview Links
-- Created: 2025-12-19
-- Purpose: Add tables for "Build My Site" viral wedge functionality

-- ============================================================
-- ENUMS
-- ============================================================

-- Build session status enum
CREATE TYPE build_session_status AS ENUM (
'created',
'previewed',
'locked',
'converted'
);

-- Build session input type enum
CREATE TYPE build_session_input_type AS ENUM (
'idea',
'url'
);

-- ============================================================
-- TABLES
-- ============================================================

-- Build sessions table
-- Stores each "Build My Site" session (anonymous or authenticated)
CREATE TABLE IF NOT EXISTS build_sessions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

-- Session data
language TEXT NOT NULL DEFAULT 'en', -- 'en' | 'es'
input_type build_session_input_type NOT NULL,
input_value TEXT NOT NULL,

-- Audit results (static for Phase 2, real in Phase 3)
teaser_score INTEGER,
teaser_summary JSONB,

-- Status tracking
status build_session_status NOT NULL DEFAULT 'created',

-- User relationship (nullable - anonymous sessions allowed)
user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL ON UPDATE CASCADE
);

-- Preview links table
-- Public shareable links for build sessions
CREATE TABLE IF NOT EXISTS preview_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),

-- Build session relationship
build_session_id UUID NOT NULL REFERENCES build_sessions(id) ON DELETE CASCADE ON UPDATE CASCADE,

-- Public slug for sharing (unguessable)
slug TEXT NOT NULL UNIQUE,

-- Optional expiration
expires_at TIMESTAMPTZ
);

-- ============================================================
-- INDEXES
-- ============================================================

-- Index on preview_links.slug for fast lookup
CREATE INDEX idx_preview_links_slug ON preview_links(slug);

-- Index on build_sessions.user_id for user session queries
CREATE INDEX idx_build_sessions_user_id ON build_sessions(user_id);

-- Index on build_sessions.status for status filtering
CREATE INDEX idx_build_sessions_status ON build_sessions(status);

-- ============================================================
-- ROW LEVEL SECURITY (RLS) POLICIES
-- ============================================================

-- Enable RLS on both tables
ALTER TABLE build_sessions ENABLE ROW LEVEL SECURITY;
ALTER TABLE preview_links ENABLE ROW LEVEL SECURITY;

-- ============================================================
-- BUILD_SESSIONS POLICIES
-- ============================================================

-- RLS Policy: Public insert allowed (anonymous users can create sessions)
-- Rationale: "No signup to start" - anyone can create a build session
DROP POLICY IF EXISTS "build_sessions_insert_policy" ON build_sessions;
CREATE POLICY "build_sessions_insert_policy" ON build_sessions
FOR INSERT
TO anon, authenticated
WITH CHECK (true);

-- RLS Policy: Public select DENIED (no public listing of sessions)
-- Rationale: Sessions are private by default, only accessible via preview link
DROP POLICY IF EXISTS "build_sessions_select_anon_policy" ON build_sessions;
CREATE POLICY "build_sessions_select_anon_policy" ON build_sessions
FOR SELECT
TO anon
USING (false);

-- RLS Policy: Owner select allowed (users can see their own sessions)
-- Rationale: Authenticated users can view sessions they created
DROP POLICY IF EXISTS "build_sessions_select_owner_policy" ON build_sessions;
CREATE POLICY "build_sessions_select_owner_policy" ON build_sessions
FOR SELECT
TO authenticated
USING (user_id = auth.uid());

-- RLS Policy: Owner update allowed (users can update their own sessions)
-- Rationale: Users can change status or claim anonymous sessions
DROP POLICY IF EXISTS "build_sessions_update_owner_policy" ON build_sessions;
CREATE POLICY "build_sessions_update_owner_policy" ON build_sessions
FOR UPDATE
TO authenticated
USING (user_id = auth.uid() OR user_id IS NULL)
Comment on lines +116 to +121

Copilot AI Dec 27, 2025

Copy link

Choose a reason for hiding this comment

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

The build_sessions_update_owner_policy allows any authenticated user to update rows where user_id is NULL, which means a malicious user can "claim" any anonymous build session by setting its user_id to their own and then read or manipulate that session’s data. With the Supabase anon key exposed in the browser, an attacker can sign in, enumerate target build_session_ids via related APIs (e.g., from preview links), and issue update calls that pass RLS (user_id IS NULL), effectively hijacking other users’ build sessions. Tighten this policy so only the creator or a server-side service role can associate a session with a user (for example by tying updates to a per-session secret or only allowing user assignment in trusted backend code), and avoid permitting arbitrary updates on user_id IS NULL rows from client-side roles.

Suggested change
-- Rationale: Users can change status or claim anonymous sessions
DROP POLICY IF EXISTS "build_sessions_update_owner_policy" ON build_sessions;
CREATE POLICY "build_sessions_update_owner_policy" ON build_sessions
FOR UPDATE
TO authenticated
USING (user_id = auth.uid() OR user_id IS NULL)
-- Rationale: Authenticated users can only update sessions they own; associating
-- anonymous sessions with a user must be done via trusted backend code
DROP POLICY IF EXISTS "build_sessions_update_owner_policy" ON build_sessions;
CREATE POLICY "build_sessions_update_owner_policy" ON build_sessions
FOR UPDATE
TO authenticated
USING (user_id = auth.uid())

Copilot uses AI. Check for mistakes.
WITH CHECK (user_id = auth.uid());

-- ============================================================
-- PREVIEW_LINKS POLICIES
-- ============================================================

-- RLS Policy: Public select by slug allowed (anyone can view via preview link)
-- Rationale: Preview links are publicly shareable - this is the viral mechanic
DROP POLICY IF EXISTS "preview_links_select_by_slug_policy" ON preview_links;
CREATE POLICY "preview_links_select_by_slug_policy" ON preview_links
FOR SELECT
TO anon, authenticated
USING (true);
Comment on lines +130 to +134

Copilot AI Dec 27, 2025

Copy link

Choose a reason for hiding this comment

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

The preview_links_select_by_slug_policy grants SELECT on all preview_links rows to both anon and authenticated roles with USING (true), which lets anyone holding the public Supabase anon key enumerate every preview slug and associated build_session_id. This breaks the intended "unguessable slug, no enumeration" security model and allows an attacker to list all share links and then fetch each preview (and, in combination with the build session update policy, potentially take over those sessions). Restrict SELECT so that public roles can only read a single row by a provided slug through a tightly scoped backend endpoint or RPC (and not via unrestricted table SELECT), and ensure expired links and any sensitive fields are filtered server-side.

Copilot uses AI. Check for mistakes.

-- RLS Policy: No public insert (only server/authenticated can create)
-- Rationale: Prevent spam, only app can generate preview links
DROP POLICY IF EXISTS "preview_links_insert_policy" ON preview_links;
CREATE POLICY "preview_links_insert_policy" ON preview_links
FOR INSERT
TO authenticated
WITH CHECK (true);
Comment on lines +136 to +142

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

preview_links_insert_policy blocks anonymous session creation flow.

The policy only allows authenticated users to insert preview links, but the application flow (per build-session.ts) creates preview links for anonymous users. This will cause inserts to fail for anonymous sessions unless the insert bypasses RLS (e.g., using a service role).

🔎 Potential fixes

Option 1: Allow anon to insert (if acceptable):

 CREATE POLICY "preview_links_insert_policy" ON preview_links
 FOR INSERT
-TO authenticated
+TO anon, authenticated
 WITH CHECK (true);

Option 2: Ensure the tRPC router uses a service role connection that bypasses RLS for anonymous session creation. Verify this is the intended design.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
-- RLS Policy: No public insert (only server/authenticated can create)
-- Rationale: Prevent spam, only app can generate preview links
DROP POLICY IF EXISTS "preview_links_insert_policy" ON preview_links;
CREATE POLICY "preview_links_insert_policy" ON preview_links
FOR INSERT
TO authenticated
WITH CHECK (true);
-- RLS Policy: No public insert (only server/authenticated can create)
-- Rationale: Prevent spam, only app can generate preview links
DROP POLICY IF EXISTS "preview_links_insert_policy" ON preview_links;
CREATE POLICY "preview_links_insert_policy" ON preview_links
FOR INSERT
TO anon, authenticated
WITH CHECK (true);


-- RLS Policy: No public update (immutable once created)
-- Rationale: Preview links should not change after creation
DROP POLICY IF EXISTS "preview_links_update_policy" ON preview_links;
CREATE POLICY "preview_links_update_policy" ON preview_links
FOR UPDATE
TO authenticated
USING (false);

-- RLS Policy: No public delete (only via cascade from build_session)
-- Rationale: Cleanup happens automatically when session is deleted
DROP POLICY IF EXISTS "preview_links_delete_policy" ON preview_links;
CREATE POLICY "preview_links_delete_policy" ON preview_links
FOR DELETE
TO authenticated
USING (
EXISTS (
SELECT 1 FROM build_sessions
WHERE build_sessions.id = preview_links.build_session_id
AND build_sessions.user_id = auth.uid()
)
);

-- ============================================================
-- SECURITY NOTES
-- ============================================================
--
-- build_sessions:
-- - Public INSERT: Anyone can create (viral onboarding)
-- - Public SELECT: DENIED (no data leaks)
-- - Owner SELECT/UPDATE: User can see and update their own sessions
-- - Anonymous sessions can be "claimed" by authenticated users
--
-- preview_links:
-- - Public SELECT: Anyone with slug can view (shareable)
-- - Public INSERT: DENIED (only app creates)
-- - Public UPDATE/DELETE: DENIED (immutable)
-- - No listing endpoint (must know exact slug)
--
-- Privacy guarantees:
-- - Slug must be unguessable (min 8 chars, random)
-- - No enumeration attack possible (no public list)
-- - User data not exposed via preview (sanitized in query)
-- - Expired links can be checked client-side before rendering
--
-- ============================================================

-- Add comment metadata
COMMENT ON TABLE build_sessions IS 'Phase 2: Build My Site sessions (anonymous or authenticated)';
COMMENT ON TABLE preview_links IS 'Phase 2: Public shareable preview links for build sessions';
COMMENT ON COLUMN build_sessions.user_id IS 'Nullable - anonymous sessions allowed, can be claimed later';
COMMENT ON COLUMN preview_links.slug IS 'Unguessable random slug for public sharing (min 8 chars)';
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
-- PHASE 4: Add Credit Balances and Fix Packs
-- Created: 2025-12-19

Copilot AI Dec 27, 2025

Copy link

Choose a reason for hiding this comment

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

The migration filename contains a future date (2025-12-19) but the current date is December 27, 2025. Migration files should use the actual creation date to maintain proper ordering and prevent confusion about when changes were made.

Suggested change
-- Created: 2025-12-19
-- Created: 2025-12-27

Copilot uses AI. Check for mistakes.
-- Purpose: Monetization, credits, and Fix Pack application

-- ============================================================
-- ENUMS
-- ============================================================

-- Cynthia plan enum
CREATE TYPE cynthia_plan AS ENUM (
'free',
'starter',
'pro',
'agency'
);

-- Fix pack type enum
CREATE TYPE fix_pack_type AS ENUM (
'token',
'layout',
'component',
'motion',
'content'
);

-- ============================================================
-- TABLES
-- ============================================================

-- Credit balances table
CREATE TABLE credit_balances (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
user_id UUID NOT NULL UNIQUE REFERENCES auth.users(id) ON DELETE CASCADE ON UPDATE CASCADE,
plan cynthia_plan NOT NULL DEFAULT 'free',
monthly_credits INTEGER NOT NULL DEFAULT 0,
used_credits INTEGER NOT NULL DEFAULT 0,
reset_at TIMESTAMPTZ NOT NULL
);

-- Fix packs table
CREATE TABLE fix_packs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
audit_id UUID NOT NULL REFERENCES cynthia_audits(id) ON DELETE CASCADE ON UPDATE CASCADE,
user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE ON UPDATE CASCADE,
type fix_pack_type NOT NULL,
title TEXT NOT NULL,
description TEXT NOT NULL,
patch_preview JSONB NOT NULL,
files_affected JSONB NOT NULL,
issues_fixed JSONB NOT NULL,
applied_at TIMESTAMPTZ
);

-- ============================================================
-- INDEXES
-- ============================================================

-- Credit balances indexes
CREATE INDEX idx_credit_balances_user_id ON credit_balances(user_id);
CREATE INDEX idx_credit_balances_reset_at ON credit_balances(reset_at);

-- Fix packs indexes
CREATE INDEX idx_fix_packs_audit_id ON fix_packs(audit_id);
CREATE INDEX idx_fix_packs_user_id ON fix_packs(user_id);
CREATE INDEX idx_fix_packs_type ON fix_packs(type);
CREATE INDEX idx_fix_packs_applied_at ON fix_packs(applied_at);

-- ============================================================
-- ROW LEVEL SECURITY (RLS)
-- ============================================================

-- Enable RLS on credit_balances
ALTER TABLE credit_balances ENABLE ROW LEVEL SECURITY;

-- Credit balances: Users can view their own balance
CREATE POLICY "credit_balances_select_own_policy" ON credit_balances
FOR SELECT TO authenticated
USING (user_id = auth.uid());

-- Credit balances: No public insert (only system/admin)
CREATE POLICY "credit_balances_insert_policy" ON credit_balances
FOR INSERT TO authenticated
WITH CHECK (false); -- System only

-- Credit balances: No public update (only system/admin)
CREATE POLICY "credit_balances_update_policy" ON credit_balances
FOR UPDATE TO authenticated
USING (false); -- System only

-- Enable RLS on fix_packs
ALTER TABLE fix_packs ENABLE ROW LEVEL SECURITY;

-- Fix packs: Users can view their own fix packs
CREATE POLICY "fix_packs_select_own_policy" ON fix_packs
FOR SELECT TO authenticated
USING (user_id = auth.uid());

-- Fix packs: No public insert (only system)
CREATE POLICY "fix_packs_insert_policy" ON fix_packs
FOR INSERT TO authenticated
WITH CHECK (false); -- System only

-- Fix packs: No public update
CREATE POLICY "fix_packs_update_policy" ON fix_packs
FOR UPDATE TO authenticated
USING (false); -- System only
Comment on lines +79 to +110

Copilot AI Dec 27, 2025

Copy link

Choose a reason for hiding this comment

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

The RLS policy for credit_balances prevents all authenticated users from inserting or updating records (WITH CHECK false). However, the service functions in credit-service.ts use the application's database client which bypasses RLS. This creates a security model where only service-level code can modify credits, not direct database access.

Add a comment explaining this is intentional and that all credit operations must go through the service layer, or consider adding a service role policy if the application needs programmatic access.

Suggested change
-- Credit balances: Users can view their own balance
CREATE POLICY "credit_balances_select_own_policy" ON credit_balances
FOR SELECT TO authenticated
USING (user_id = auth.uid());
-- Credit balances: No public insert (only system/admin)
CREATE POLICY "credit_balances_insert_policy" ON credit_balances
FOR INSERT TO authenticated
WITH CHECK (false); -- System only
-- Credit balances: No public update (only system/admin)
CREATE POLICY "credit_balances_update_policy" ON credit_balances
FOR UPDATE TO authenticated
USING (false); -- System only
-- Enable RLS on fix_packs
ALTER TABLE fix_packs ENABLE ROW LEVEL SECURITY;
-- Fix packs: Users can view their own fix packs
CREATE POLICY "fix_packs_select_own_policy" ON fix_packs
FOR SELECT TO authenticated
USING (user_id = auth.uid());
-- Fix packs: No public insert (only system)
CREATE POLICY "fix_packs_insert_policy" ON fix_packs
FOR INSERT TO authenticated
WITH CHECK (false); -- System only
-- Fix packs: No public update
CREATE POLICY "fix_packs_update_policy" ON fix_packs
FOR UPDATE TO authenticated
USING (false); -- System only
-- NOTE: RLS for credit_balances is intentionally restrictive.
-- Authenticated users may only SELECT their own balances; INSERT/UPDATE
-- are blocked (USING/WITH CHECK false) so that only the backend service
-- layer, using a privileged/service-role database client that bypasses RLS,
-- can modify credit balances. All credit mutations must go through the
-- service layer. If direct programmatic DB access is needed in the future,
-- add an explicit service-role policy instead of relaxing these checks.
-- Credit balances: Users can view their own balance
CREATE POLICY "credit_balances_select_own_policy" ON credit_balances
FOR SELECT TO authenticated
USING (user_id = auth.uid());
-- Credit balances: No public insert (only system/admin via service layer)
CREATE POLICY "credit_balances_insert_policy" ON credit_balances
FOR INSERT TO authenticated
WITH CHECK (false); -- System only; see note above about service-layer writes
-- Credit balances: No public update (only system/admin via service layer)
CREATE POLICY "credit_balances_update_policy" ON credit_balances
FOR UPDATE TO authenticated
USING (false); -- System only; see note above about service-layer writes
-- Enable RLS on fix_packs
ALTER TABLE fix_packs ENABLE ROW LEVEL SECURITY;
-- NOTE: As with credit_balances, fix_packs writes are restricted so that
-- only the backend service layer or privileged roles can create/update
-- records; authenticated clients can only read their own fix_packs.
-- Fix packs: Users can view their own fix packs
CREATE POLICY "fix_packs_select_own_policy" ON fix_packs
FOR SELECT TO authenticated
USING (user_id = auth.uid());
-- Fix packs: No public insert (only system via service layer)
CREATE POLICY "fix_packs_insert_policy" ON fix_packs
FOR INSERT TO authenticated
WITH CHECK (false); -- System only; see note above about service-layer writes
-- Fix packs: No public update
CREATE POLICY "fix_packs_update_policy" ON fix_packs
FOR UPDATE TO authenticated
USING (false); -- System only; see note above about service-layer writes

Copilot uses AI. Check for mistakes.

-- ============================================================
-- FUNCTIONS
-- ============================================================

-- Function to reset monthly credits (for cron job)
CREATE OR REPLACE FUNCTION reset_monthly_credits()
RETURNS void
LANGUAGE plpgsql
SECURITY DEFINER
AS $$
BEGIN
UPDATE credit_balances
SET
used_credits = 0,
reset_at = reset_at + INTERVAL '1 month',
updated_at = now()
WHERE reset_at <= now();
END;
$$;

Copilot AI Dec 27, 2025

Copy link

Choose a reason for hiding this comment

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

The reset_monthly_credits function is declared SECURITY DEFINER and performs an unrestricted UPDATE on all credit_balances rows without checking auth.uid() or caller role, so any caller with execute privileges (e.g., via Supabase RPC using the public anon/authenticated roles) can reset every user’s used credits and shift reset_at. This bypasses the RLS protections on credit_balances and enables unauthorized global modification of billing/credit state, which an attacker could abuse to repeatedly zero out their own usage (and everyone else’s) outside of the intended monthly cycle. Lock this down by revoking EXECUTE from untrusted roles and/or adding explicit checks inside the function to ensure only a trusted service role or scheduled job can invoke it, rather than exposing it to general client-facing roles.

Suggested change
-- Restrict execution of this SECURITY DEFINER function to trusted roles only.
-- By revoking EXECUTE from PUBLIC, client-facing roles (e.g., anon/authenticated)
-- cannot call this function unless explicitly granted elsewhere.
REVOKE EXECUTE ON FUNCTION reset_monthly_credits() FROM PUBLIC;

Copilot uses AI. Check for mistakes.
-- ============================================================
-- COMMENTS
-- ============================================================

COMMENT ON TABLE credit_balances IS 'Phase 4: Tracks Cynthia Build My Site credit balances per user';
COMMENT ON TABLE fix_packs IS 'Phase 4: Stores generated fix packs from Cynthia audits';

COMMENT ON COLUMN credit_balances.plan IS 'Cynthia plan tier: free (0), starter (10), pro (50), agency (200)';
COMMENT ON COLUMN credit_balances.monthly_credits IS 'Total credits allocated per month based on plan';
COMMENT ON COLUMN credit_balances.used_credits IS 'Credits consumed in current period';
COMMENT ON COLUMN credit_balances.reset_at IS 'Next monthly reset date';

COMMENT ON COLUMN fix_packs.type IS 'Fix pack type: token, layout, component, motion, or content';
COMMENT ON COLUMN fix_packs.patch_preview IS 'Code diff preview (read-only in Phase 4)';
COMMENT ON COLUMN fix_packs.applied_at IS 'NULL = not applied yet, timestamp = applied (Phase 5 will populate)';
Loading