From 43708fd088a2b1977fb35568c9c91fedcdd1a007 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Tue, 24 Mar 2026 20:05:12 -0700 Subject: [PATCH] feat: add screenshot storage migration + web URL config Supabase migration 004 creates: - pr-screenshots storage bucket (private, service_role read) - screenshots table with RLS (auth insert, public read metadata) - device_codes table for RFC 8628 auth flow (service_role only) - pg_cron cleanup for expired codes and orphan screenshots Also adds GSTACK_WEB_URL to config.sh for gstack.gg integration. Co-Authored-By: Claude Opus 4.6 (1M context) --- supabase/config.sh | 3 + .../migrations/004_screenshot_storage.sql | 96 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 supabase/migrations/004_screenshot_storage.sql diff --git a/supabase/config.sh b/supabase/config.sh index b10aef6b..0d41a0e6 100644 --- a/supabase/config.sh +++ b/supabase/config.sh @@ -8,3 +8,6 @@ GSTACK_SUPABASE_ANON_KEY="sb_publishable_tR4i6cyMIrYTE3s6OyHGHw_ppx2p6WK" # Telemetry ingest endpoint (Data API) GSTACK_TELEMETRY_ENDPOINT="${GSTACK_SUPABASE_URL}/rest/v1" + +# gstack.gg web app (auth + screenshot upload) +GSTACK_WEB_URL="https://gstack.gg" diff --git a/supabase/migrations/004_screenshot_storage.sql b/supabase/migrations/004_screenshot_storage.sql new file mode 100644 index 00000000..55196561 --- /dev/null +++ b/supabase/migrations/004_screenshot_storage.sql @@ -0,0 +1,96 @@ +-- 004_screenshot_storage.sql +-- PR screenshot storage + device code auth for CLI → web auth flow + +-- ─── Storage bucket (PRIVATE — proxy adds watermark) ───────────── +INSERT INTO storage.buckets (id, name, public) +VALUES ('pr-screenshots', 'pr-screenshots', false) +ON CONFLICT (id) DO NOTHING; + +-- Storage RLS: authenticated users upload to their own prefix +CREATE POLICY "auth_upload_own_prefix" ON storage.objects + FOR INSERT TO authenticated + WITH CHECK (bucket_id = 'pr-screenshots' AND (storage.foldername(name))[1] = auth.uid()::text); + +-- Storage RLS: service_role reads (proxy fetches via service key) +-- No public read — raw images must go through watermark proxy +CREATE POLICY "service_read_screenshots" ON storage.objects + FOR SELECT TO service_role + USING (bucket_id = 'pr-screenshots'); + +-- Storage RLS: authenticated users can delete their own uploads +CREATE POLICY "auth_delete_own" ON storage.objects + FOR DELETE TO authenticated + USING (bucket_id = 'pr-screenshots' AND (storage.foldername(name))[1] = auth.uid()::text); + +-- ─── Screenshots metadata table ────────────────────────────────── +CREATE TABLE IF NOT EXISTS screenshots ( + id TEXT PRIMARY KEY, -- 8-char nanoid + user_id UUID NOT NULL REFERENCES auth.users(id), + storage_path TEXT NOT NULL, -- path in pr-screenshots bucket + repo_slug TEXT NOT NULL, -- slugified repo name + branch TEXT NOT NULL, -- slugified branch name + viewport TEXT, -- e.g. 'mobile', 'tablet', 'desktop' + pr_number INTEGER, -- populated after PR creation + created_at TIMESTAMPTZ NOT NULL DEFAULT now() +); + +-- Indexes +CREATE INDEX idx_screenshots_user ON screenshots(user_id); +CREATE INDEX idx_screenshots_repo_branch ON screenshots(repo_slug, branch); + +-- RLS on screenshots: auth insert own, public read metadata, auth delete own +ALTER TABLE screenshots ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "auth_insert_own_screenshots" ON screenshots + FOR INSERT TO authenticated + WITH CHECK (user_id = auth.uid()); + +CREATE POLICY "public_read_screenshots" ON screenshots + FOR SELECT TO anon, authenticated + USING (true); + +CREATE POLICY "auth_delete_own_screenshots" ON screenshots + FOR DELETE TO authenticated + USING (user_id = auth.uid()); + +-- ─── Device codes table (RFC 8628 pattern) ─────────────────────── +CREATE TABLE IF NOT EXISTS device_codes ( + code TEXT PRIMARY KEY, -- server-generated device code + device_secret TEXT NOT NULL, -- PKCE-like secret for verification + user_code TEXT NOT NULL, -- short human-readable code (e.g. ABCD-1234) + user_id UUID REFERENCES auth.users(id), -- NULL until user approves + status TEXT NOT NULL DEFAULT 'pending', -- pending | approved | expired + created_at TIMESTAMPTZ NOT NULL DEFAULT now(), + expires_at TIMESTAMPTZ NOT NULL -- 10 minutes from creation +); + +-- Index for polling (CLI polls by device_code + secret) +CREATE INDEX idx_device_codes_status ON device_codes(code, status); + +-- RLS: service_role only (all access goes through API routes) +ALTER TABLE device_codes ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "service_only_device_codes" ON device_codes + FOR ALL TO service_role + USING (true) + WITH CHECK (true); + +-- ─── Cleanup: expired device codes + orphan screenshots ────────── +-- Delete expired device codes (> 15 minutes old, generous buffer over 10min expiry) +-- Delete orphan screenshots (no PR number after 24h) +-- Run via pg_cron if available, otherwise manual/API trigger +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_extension WHERE extname = 'pg_cron') THEN + PERFORM cron.schedule( + 'cleanup_device_codes', + '*/15 * * * *', -- every 15 minutes + $$DELETE FROM device_codes WHERE expires_at < now() - interval '5 minutes'$$ + ); + PERFORM cron.schedule( + 'cleanup_orphan_screenshots', + '0 */6 * * *', -- every 6 hours + $$DELETE FROM screenshots WHERE pr_number IS NULL AND created_at < now() - interval '24 hours'$$ + ); + END IF; +END $$;