mirror of https://github.com/garrytan/gstack.git
test: RLS smoke test + telemetry field name verification
- verify-rls.sh: 9-check smoke test (5 reads + 3 inserts + 1 update) verifying anon key is fully locked out after migration. - telemetry.test.ts: verifies JSONL uses raw field names (v, ts, sessions) that the edge function expects, not Postgres column names. - README.md: fixes privacy claim to match actual RLS policy.
This commit is contained in:
parent
12d3a6a18c
commit
3330b8e68d
|
|
@ -212,7 +212,7 @@ gstack includes **opt-in** usage telemetry to help improve the project. Here's e
|
||||||
- **What's never sent:** code, file paths, repo names, branch names, prompts, or any user-generated content.
|
- **What's never sent:** code, file paths, repo names, branch names, prompts, or any user-generated content.
|
||||||
- **Change anytime:** `gstack-config set telemetry off` disables everything instantly.
|
- **Change anytime:** `gstack-config set telemetry off` disables everything instantly.
|
||||||
|
|
||||||
Data is stored in [Supabase](https://supabase.com) (open source Firebase alternative). The schema is in [`supabase/migrations/001_telemetry.sql`](supabase/migrations/001_telemetry.sql) — you can verify exactly what's collected. The Supabase publishable key in the repo is a public key (like a Firebase API key) — row-level security policies restrict it to insert-only access.
|
Data is stored in [Supabase](https://supabase.com) (open source Firebase alternative). The schema is in [`supabase/migrations/`](supabase/migrations/) — you can verify exactly what's collected. The Supabase publishable key in the repo is a public key (like a Firebase API key) — row-level security policies deny all direct access. Telemetry flows through validated edge functions that enforce schema checks, event type allowlists, and field length limits.
|
||||||
|
|
||||||
**Local analytics are always available.** Run `gstack-analytics` to see your personal usage dashboard from the local JSONL file — no remote data needed.
|
**Local analytics are always available.** Run `gstack-analytics` to see your personal usage dashboard from the local JSONL file — no remote data needed.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,96 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# verify-rls.sh — smoke test that anon key is locked out after 002_tighten_rls.sql
|
||||||
|
#
|
||||||
|
# Run manually after deploying the migration:
|
||||||
|
# bash supabase/verify-rls.sh
|
||||||
|
#
|
||||||
|
# All 9 checks should PASS (anon key denied for reads AND writes).
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
|
. "$SCRIPT_DIR/config.sh"
|
||||||
|
|
||||||
|
URL="$GSTACK_SUPABASE_URL"
|
||||||
|
KEY="$GSTACK_SUPABASE_ANON_KEY"
|
||||||
|
PASS=0
|
||||||
|
FAIL=0
|
||||||
|
|
||||||
|
check() {
|
||||||
|
local desc="$1"
|
||||||
|
local method="$2"
|
||||||
|
local path="$3"
|
||||||
|
local data="${4:-}"
|
||||||
|
|
||||||
|
local args=(-sf -o /dev/null -w '%{http_code}' --max-time 10
|
||||||
|
-H "apikey: ${KEY}"
|
||||||
|
-H "Content-Type: application/json")
|
||||||
|
|
||||||
|
if [ "$method" = "GET" ]; then
|
||||||
|
HTTP="$(curl "${args[@]}" "${URL}/rest/v1/${path}" 2>/dev/null || echo "000")"
|
||||||
|
elif [ "$method" = "POST" ]; then
|
||||||
|
HTTP="$(curl "${args[@]}" -X POST "${URL}/rest/v1/${path}" -H "Prefer: return=minimal" -d "$data" 2>/dev/null || echo "000")"
|
||||||
|
elif [ "$method" = "PATCH" ]; then
|
||||||
|
HTTP="$(curl "${args[@]}" -X PATCH "${URL}/rest/v1/${path}" -d "$data" 2>/dev/null || echo "000")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Success = anything that is NOT a 200/201 with data
|
||||||
|
# 403, 401, or empty 200 (=[]) all count as "denied"
|
||||||
|
case "$HTTP" in
|
||||||
|
200)
|
||||||
|
# For GETs, check if response is empty array
|
||||||
|
BODY="$(curl -sf --max-time 10 "${URL}/rest/v1/${path}" -H "apikey: ${KEY}" -H "Content-Type: application/json" 2>/dev/null || echo "")"
|
||||||
|
if [ "$BODY" = "[]" ] || [ -z "$BODY" ]; then
|
||||||
|
echo " PASS $desc (HTTP $HTTP, empty)"
|
||||||
|
PASS=$(( PASS + 1 ))
|
||||||
|
else
|
||||||
|
echo " FAIL $desc (HTTP $HTTP, got data)"
|
||||||
|
FAIL=$(( FAIL + 1 ))
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
401|403|404|406)
|
||||||
|
echo " PASS $desc (HTTP $HTTP, denied)"
|
||||||
|
PASS=$(( PASS + 1 ))
|
||||||
|
;;
|
||||||
|
201)
|
||||||
|
echo " FAIL $desc (HTTP $HTTP, write succeeded!)"
|
||||||
|
FAIL=$(( FAIL + 1 ))
|
||||||
|
;;
|
||||||
|
000)
|
||||||
|
echo " WARN $desc (connection failed)"
|
||||||
|
FAIL=$(( FAIL + 1 ))
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo " PASS $desc (HTTP $HTTP)"
|
||||||
|
PASS=$(( PASS + 1 ))
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "RLS Lockdown Verification"
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo ""
|
||||||
|
echo "Read denial checks:"
|
||||||
|
check "SELECT telemetry_events" GET "telemetry_events?select=*&limit=1"
|
||||||
|
check "SELECT installations" GET "installations?select=*&limit=1"
|
||||||
|
check "SELECT update_checks" GET "update_checks?select=*&limit=1"
|
||||||
|
check "SELECT crash_clusters" GET "crash_clusters?select=*&limit=1"
|
||||||
|
check "SELECT skill_sequences" GET "skill_sequences?select=skill_a&limit=1"
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "Write denial checks:"
|
||||||
|
check "INSERT telemetry_events" POST "telemetry_events" '{"gstack_version":"test","os":"test","event_timestamp":"2026-01-01T00:00:00Z","outcome":"test"}'
|
||||||
|
check "INSERT update_checks" POST "update_checks" '{"gstack_version":"test","os":"test"}'
|
||||||
|
check "INSERT installations" POST "installations" '{"installation_id":"test_verify_rls"}'
|
||||||
|
check "UPDATE installations" PATCH "installations?installation_id=eq.test_verify_rls" '{"gstack_version":"hacked"}'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
|
||||||
|
echo "Results: $PASS passed, $FAIL failed (of 9 checks)"
|
||||||
|
|
||||||
|
if [ "$FAIL" -gt 0 ]; then
|
||||||
|
echo "VERDICT: FAIL — anon key still has access"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "VERDICT: PASS — anon key fully locked out"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
@ -244,16 +244,32 @@ describe('gstack-analytics', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('gstack-telemetry-sync', () => {
|
describe('gstack-telemetry-sync', () => {
|
||||||
test('exits silently with no endpoint configured', () => {
|
test('exits silently with no Supabase URL configured', () => {
|
||||||
// Default: GSTACK_TELEMETRY_ENDPOINT is not set → exit 0
|
// Default: GSTACK_SUPABASE_URL is not set → exit 0
|
||||||
const result = run(`${BIN}/gstack-telemetry-sync`);
|
const result = run(`${BIN}/gstack-telemetry-sync`);
|
||||||
expect(result).toBe('');
|
expect(result).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('exits silently with no JSONL file', () => {
|
test('exits silently with no JSONL file', () => {
|
||||||
const result = run(`${BIN}/gstack-telemetry-sync`, { GSTACK_TELEMETRY_ENDPOINT: 'http://localhost:9999' });
|
const result = run(`${BIN}/gstack-telemetry-sync`, { GSTACK_SUPABASE_URL: 'http://localhost:9999' });
|
||||||
expect(result).toBe('');
|
expect(result).toBe('');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('does not rename JSONL field names (edge function expects raw names)', () => {
|
||||||
|
setConfig('telemetry', 'anonymous');
|
||||||
|
run(`${BIN}/gstack-telemetry-log --skill qa --duration 60 --outcome success --session-id raw-fields-1`);
|
||||||
|
|
||||||
|
const events = parseJsonl();
|
||||||
|
expect(events).toHaveLength(1);
|
||||||
|
// Edge function expects these raw field names, NOT Postgres column names
|
||||||
|
expect(events[0]).toHaveProperty('v');
|
||||||
|
expect(events[0]).toHaveProperty('ts');
|
||||||
|
expect(events[0]).toHaveProperty('sessions');
|
||||||
|
// Should NOT have Postgres column names
|
||||||
|
expect(events[0]).not.toHaveProperty('schema_version');
|
||||||
|
expect(events[0]).not.toHaveProperty('event_timestamp');
|
||||||
|
expect(events[0]).not.toHaveProperty('concurrent_sessions');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('gstack-community-dashboard', () => {
|
describe('gstack-community-dashboard', () => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue