ship-safe 1.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,50 @@
1
+ # .ship-safeignore
2
+ # =================
3
+ # Exclude paths from ship-safe secret scanning.
4
+ # Same syntax as .gitignore — one pattern per line, # for comments.
5
+ #
6
+ # Ship-safe already skips test files by default.
7
+ # Use this file to exclude additional paths that generate false positives.
8
+ #
9
+ # USAGE:
10
+ # Copy this file to your project root as .ship-safeignore
11
+ # Then run: npx ship-safe scan .
12
+
13
+ # ── Examples ──────────────────────────────────────────────────────────────────
14
+
15
+ # Exclude a specific file
16
+ # config/seed-data.js
17
+
18
+ # Exclude a directory
19
+ # scripts/fixtures/
20
+
21
+ # Exclude all files matching a pattern
22
+ # **/*.example.js
23
+ # **/*.sample.*
24
+
25
+ # Exclude documentation that contains example credentials
26
+ # docs/
27
+ # *.md
28
+
29
+ # Exclude generated files
30
+ # generated/
31
+ # prisma/migrations/
32
+
33
+ # Exclude vendor/third-party code not in node_modules
34
+ # vendor/
35
+ # third-party/
36
+
37
+ # ── Common false positive sources ─────────────────────────────────────────────
38
+
39
+ # E2E test fixtures (ship-safe skips unit tests but not e2e by default)
40
+ # e2e/
41
+ # cypress/fixtures/
42
+ # playwright/
43
+
44
+ # Seed data with example values
45
+ # prisma/seed.ts
46
+ # database/seeds/
47
+
48
+ # Infrastructure-as-code with example configs
49
+ # terraform/examples/
50
+ # .terraform/
@@ -0,0 +1,242 @@
1
+ -- =============================================================================
2
+ -- SUPABASE ROW LEVEL SECURITY (RLS) TEMPLATES
3
+ -- =============================================================================
4
+ --
5
+ -- Copy-paste these policies to secure your Supabase tables.
6
+ --
7
+ -- WHY RLS MATTERS:
8
+ -- Without RLS, anyone with your anon key can read/write ALL data.
9
+ -- 83% of exposed Supabase databases have RLS misconfigurations.
10
+ -- Source: https://byteiota.com/supabase-security-flaw-170-apps-exposed-by-missing-rls/
11
+ --
12
+ -- HOW TO USE:
13
+ -- 1. Enable RLS on your table: ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
14
+ -- 2. Copy the relevant policy below
15
+ -- 3. Replace 'your_table' with your actual table name
16
+ -- 4. Test with different user contexts
17
+ --
18
+ -- =============================================================================
19
+
20
+
21
+ -- =============================================================================
22
+ -- STEP 1: ALWAYS ENABLE RLS FIRST
23
+ -- =============================================================================
24
+
25
+ -- Enable RLS on a table (REQUIRED before adding policies)
26
+ ALTER TABLE your_table ENABLE ROW LEVEL SECURITY;
27
+
28
+ -- Force RLS for table owners too (recommended for security)
29
+ ALTER TABLE your_table FORCE ROW LEVEL SECURITY;
30
+
31
+
32
+ -- =============================================================================
33
+ -- PATTERN 1: USER OWNS THEIR DATA
34
+ -- =============================================================================
35
+ -- Use when: Each user should only see/edit their own records
36
+ -- Example tables: profiles, settings, user_preferences
37
+
38
+ -- Users can only SELECT their own rows
39
+ CREATE POLICY "Users can view own data"
40
+ ON your_table
41
+ FOR SELECT
42
+ USING (auth.uid() = user_id);
43
+
44
+ -- Users can only INSERT rows with their own user_id
45
+ CREATE POLICY "Users can insert own data"
46
+ ON your_table
47
+ FOR INSERT
48
+ WITH CHECK (auth.uid() = user_id);
49
+
50
+ -- Users can only UPDATE their own rows
51
+ CREATE POLICY "Users can update own data"
52
+ ON your_table
53
+ FOR UPDATE
54
+ USING (auth.uid() = user_id)
55
+ WITH CHECK (auth.uid() = user_id);
56
+
57
+ -- Users can only DELETE their own rows
58
+ CREATE POLICY "Users can delete own data"
59
+ ON your_table
60
+ FOR DELETE
61
+ USING (auth.uid() = user_id);
62
+
63
+ -- COMBINED: All operations for own data (simpler but less granular)
64
+ CREATE POLICY "Users manage own data"
65
+ ON your_table
66
+ FOR ALL
67
+ USING (auth.uid() = user_id)
68
+ WITH CHECK (auth.uid() = user_id);
69
+
70
+
71
+ -- =============================================================================
72
+ -- PATTERN 2: ORGANIZATION/TEAM BASED ACCESS
73
+ -- =============================================================================
74
+ -- Use when: Users belong to orgs and should see all org data
75
+ -- Example tables: projects, documents, team_settings
76
+
77
+ -- First, create a helper function to get user's org
78
+ CREATE OR REPLACE FUNCTION get_user_org_id()
79
+ RETURNS UUID AS $$
80
+ SELECT org_id FROM profiles WHERE id = auth.uid()
81
+ $$ LANGUAGE SQL SECURITY DEFINER;
82
+
83
+ -- Users can view all data in their organization
84
+ CREATE POLICY "Org members can view org data"
85
+ ON your_table
86
+ FOR SELECT
87
+ USING (org_id = get_user_org_id());
88
+
89
+ -- Users can insert data into their organization
90
+ CREATE POLICY "Org members can insert org data"
91
+ ON your_table
92
+ FOR INSERT
93
+ WITH CHECK (org_id = get_user_org_id());
94
+
95
+ -- Only allow updates to org data
96
+ CREATE POLICY "Org members can update org data"
97
+ ON your_table
98
+ FOR UPDATE
99
+ USING (org_id = get_user_org_id())
100
+ WITH CHECK (org_id = get_user_org_id());
101
+
102
+
103
+ -- =============================================================================
104
+ -- PATTERN 3: ROLE-BASED ACCESS CONTROL (RBAC)
105
+ -- =============================================================================
106
+ -- Use when: Different users have different permission levels
107
+ -- Example: admin, editor, viewer roles
108
+
109
+ -- Helper function to check user role
110
+ CREATE OR REPLACE FUNCTION get_user_role()
111
+ RETURNS TEXT AS $$
112
+ SELECT role FROM profiles WHERE id = auth.uid()
113
+ $$ LANGUAGE SQL SECURITY DEFINER;
114
+
115
+ -- Anyone authenticated can view (public within app)
116
+ CREATE POLICY "Authenticated users can view"
117
+ ON your_table
118
+ FOR SELECT
119
+ USING (auth.role() = 'authenticated');
120
+
121
+ -- Only admins and editors can insert
122
+ CREATE POLICY "Admins and editors can insert"
123
+ ON your_table
124
+ FOR INSERT
125
+ WITH CHECK (get_user_role() IN ('admin', 'editor'));
126
+
127
+ -- Only admins can delete
128
+ CREATE POLICY "Only admins can delete"
129
+ ON your_table
130
+ FOR DELETE
131
+ USING (get_user_role() = 'admin');
132
+
133
+
134
+ -- =============================================================================
135
+ -- PATTERN 4: PUBLIC READ, AUTHENTICATED WRITE
136
+ -- =============================================================================
137
+ -- Use when: Content is public but only logged-in users can contribute
138
+ -- Example tables: blog_posts, comments, public_profiles
139
+
140
+ -- Anyone can read (including anonymous)
141
+ CREATE POLICY "Public read access"
142
+ ON your_table
143
+ FOR SELECT
144
+ USING (true);
145
+
146
+ -- Only authenticated users can insert
147
+ CREATE POLICY "Authenticated users can insert"
148
+ ON your_table
149
+ FOR INSERT
150
+ WITH CHECK (auth.role() = 'authenticated');
151
+
152
+ -- Users can only update their own posts
153
+ CREATE POLICY "Users can update own posts"
154
+ ON your_table
155
+ FOR UPDATE
156
+ USING (auth.uid() = author_id)
157
+ WITH CHECK (auth.uid() = author_id);
158
+
159
+
160
+ -- =============================================================================
161
+ -- PATTERN 5: PRIVATE BY DEFAULT (Explicit sharing)
162
+ -- =============================================================================
163
+ -- Use when: Data is private unless explicitly shared
164
+ -- Example tables: documents, files with sharing
165
+
166
+ -- Owner can always access
167
+ CREATE POLICY "Owner full access"
168
+ ON documents
169
+ FOR ALL
170
+ USING (auth.uid() = owner_id)
171
+ WITH CHECK (auth.uid() = owner_id);
172
+
173
+ -- Shared users can view (requires a shares table)
174
+ CREATE POLICY "Shared users can view"
175
+ ON documents
176
+ FOR SELECT
177
+ USING (
178
+ auth.uid() = owner_id
179
+ OR
180
+ EXISTS (
181
+ SELECT 1 FROM document_shares
182
+ WHERE document_shares.document_id = documents.id
183
+ AND document_shares.user_id = auth.uid()
184
+ )
185
+ );
186
+
187
+
188
+ -- =============================================================================
189
+ -- PATTERN 6: TIME-BASED ACCESS
190
+ -- =============================================================================
191
+ -- Use when: Content has publish dates or expiration
192
+ -- Example tables: scheduled_posts, limited_offers
193
+
194
+ -- Only show published content
195
+ CREATE POLICY "Show only published content"
196
+ ON posts
197
+ FOR SELECT
198
+ USING (
199
+ published_at IS NOT NULL
200
+ AND published_at <= NOW()
201
+ AND (expires_at IS NULL OR expires_at > NOW())
202
+ );
203
+
204
+ -- Authors can always see their drafts
205
+ CREATE POLICY "Authors see own drafts"
206
+ ON posts
207
+ FOR SELECT
208
+ USING (auth.uid() = author_id);
209
+
210
+
211
+ -- =============================================================================
212
+ -- ANTI-PATTERNS: DON'T DO THIS
213
+ -- =============================================================================
214
+
215
+ -- BAD: Allows anyone to read everything
216
+ -- CREATE POLICY "bad_policy" ON users FOR SELECT USING (true);
217
+
218
+ -- BAD: No WITH CHECK means users could insert data for other users
219
+ -- CREATE POLICY "bad_insert" ON posts FOR INSERT WITH CHECK (true);
220
+
221
+ -- BAD: Missing USING clause on UPDATE allows updating any row
222
+ -- CREATE POLICY "bad_update" ON posts FOR UPDATE WITH CHECK (auth.uid() = user_id);
223
+
224
+
225
+ -- =============================================================================
226
+ -- TESTING YOUR POLICIES
227
+ -- =============================================================================
228
+
229
+ -- Test as a specific user (in SQL editor)
230
+ -- SET request.jwt.claim.sub = 'user-uuid-here';
231
+ -- SELECT * FROM your_table;
232
+
233
+ -- Check existing policies on a table
234
+ SELECT * FROM pg_policies WHERE tablename = 'your_table';
235
+
236
+ -- List all tables without RLS enabled (DANGER!)
237
+ SELECT schemaname, tablename
238
+ FROM pg_tables
239
+ WHERE schemaname = 'public'
240
+ AND tablename NOT IN (
241
+ SELECT tablename FROM pg_policies
242
+ );
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Supabase Secure Client Configuration
3
+ * =====================================
4
+ *
5
+ * Copy this file to your project and customize for your needs.
6
+ *
7
+ * WHY THIS MATTERS:
8
+ * - Separates anon (public) and service_role (admin) clients
9
+ * - Adds type safety for environment variables
10
+ * - Includes helpers for common secure patterns
11
+ *
12
+ * USAGE:
13
+ * // In client components (React, Vue, etc.)
14
+ * import { supabase } from '@/lib/supabase';
15
+ *
16
+ * // In server-side code (API routes, server components)
17
+ * import { supabaseAdmin } from '@/lib/supabase';
18
+ */
19
+
20
+ import { createClient, SupabaseClient } from '@supabase/supabase-js';
21
+
22
+ // =============================================================================
23
+ // ENVIRONMENT VARIABLE VALIDATION
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Validates that required environment variables are set.
28
+ * Call this at app startup to fail fast if misconfigured.
29
+ */
30
+ function validateEnv() {
31
+ const required = ['NEXT_PUBLIC_SUPABASE_URL', 'NEXT_PUBLIC_SUPABASE_ANON_KEY'];
32
+
33
+ for (const key of required) {
34
+ if (!process.env[key]) {
35
+ throw new Error(
36
+ `Missing required environment variable: ${key}\n` +
37
+ `Add it to your .env.local file.`
38
+ );
39
+ }
40
+ }
41
+ }
42
+
43
+ // Validate on module load (comment out if causing issues in edge runtime)
44
+ // validateEnv();
45
+
46
+ // =============================================================================
47
+ // SUPABASE URL AND KEYS
48
+ // =============================================================================
49
+
50
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
51
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
52
+
53
+ // Service role key - ONLY available server-side
54
+ // This key bypasses RLS and should NEVER be exposed to the client
55
+ const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
56
+
57
+ // =============================================================================
58
+ // CLIENT-SIDE SUPABASE CLIENT (uses anon key)
59
+ // =============================================================================
60
+
61
+ /**
62
+ * Public Supabase client for use in browser/client components.
63
+ *
64
+ * SECURITY NOTES:
65
+ * - Uses the anon key (safe to expose)
66
+ * - All operations are subject to RLS policies
67
+ * - User must be authenticated for protected operations
68
+ */
69
+ export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
70
+ auth: {
71
+ // Persist sessions in localStorage
72
+ persistSession: true,
73
+ // Automatically refresh tokens
74
+ autoRefreshToken: true,
75
+ // Detect session from URL (for OAuth callbacks)
76
+ detectSessionInUrl: true,
77
+ },
78
+ });
79
+
80
+ // =============================================================================
81
+ // SERVER-SIDE SUPABASE CLIENT (uses service_role key)
82
+ // =============================================================================
83
+
84
+ /**
85
+ * Admin Supabase client for use in server-side code ONLY.
86
+ *
87
+ * SECURITY NOTES:
88
+ * - Uses the service_role key (NEVER expose to client)
89
+ * - Bypasses ALL Row Level Security policies
90
+ * - Use only in: API routes, server actions, cron jobs
91
+ * - Always validate user permissions before using
92
+ *
93
+ * WHEN TO USE:
94
+ * - Admin operations (deleting users, bulk updates)
95
+ * - Background jobs that need full database access
96
+ * - Webhooks from external services
97
+ *
98
+ * WHEN NOT TO USE:
99
+ * - Any client-side code
100
+ * - Operations where user context matters
101
+ * - When RLS should apply
102
+ */
103
+ export const supabaseAdmin: SupabaseClient | null = supabaseServiceRoleKey
104
+ ? createClient(supabaseUrl, supabaseServiceRoleKey, {
105
+ auth: {
106
+ // Don't persist sessions for admin client
107
+ persistSession: false,
108
+ autoRefreshToken: false,
109
+ },
110
+ })
111
+ : null;
112
+
113
+ /**
114
+ * Get admin client with error if not configured.
115
+ * Use this when service_role key is required.
116
+ */
117
+ export function getSupabaseAdmin(): SupabaseClient {
118
+ if (!supabaseAdmin) {
119
+ throw new Error(
120
+ 'SUPABASE_SERVICE_ROLE_KEY is not configured.\n' +
121
+ 'This operation requires admin privileges.'
122
+ );
123
+ }
124
+ return supabaseAdmin;
125
+ }
126
+
127
+ // =============================================================================
128
+ // HELPER: SERVER-SIDE CLIENT WITH USER CONTEXT
129
+ // =============================================================================
130
+
131
+ /**
132
+ * Creates a Supabase client that acts as a specific user.
133
+ * Useful for server-side operations that should respect RLS.
134
+ *
135
+ * @param accessToken - The user's JWT access token
136
+ * @returns Supabase client authenticated as the user
137
+ *
138
+ * USAGE:
139
+ * // In API route or server action
140
+ * const token = request.headers.get('Authorization')?.replace('Bearer ', '');
141
+ * const userClient = createUserClient(token);
142
+ * const { data } = await userClient.from('posts').select('*');
143
+ * // This respects RLS - user only sees their allowed data
144
+ */
145
+ export function createUserClient(accessToken: string): SupabaseClient {
146
+ return createClient(supabaseUrl, supabaseAnonKey, {
147
+ global: {
148
+ headers: {
149
+ Authorization: `Bearer ${accessToken}`,
150
+ },
151
+ },
152
+ auth: {
153
+ persistSession: false,
154
+ autoRefreshToken: false,
155
+ },
156
+ });
157
+ }
158
+
159
+ // =============================================================================
160
+ // HELPER: VALIDATE USER BEFORE ADMIN OPERATIONS
161
+ // =============================================================================
162
+
163
+ /**
164
+ * Validates that the current user has permission before admin operations.
165
+ * Always use this before using supabaseAdmin for user-triggered actions.
166
+ *
167
+ * @param userId - The user's ID to check
168
+ * @param requiredRole - The minimum role required (e.g., 'admin')
169
+ * @returns True if user has permission
170
+ *
171
+ * USAGE:
172
+ * const hasPermission = await validateUserPermission(userId, 'admin');
173
+ * if (!hasPermission) {
174
+ * return new Response('Forbidden', { status: 403 });
175
+ * }
176
+ * // Now safe to use supabaseAdmin
177
+ */
178
+ export async function validateUserPermission(
179
+ userId: string,
180
+ requiredRole: string
181
+ ): Promise<boolean> {
182
+ const admin = getSupabaseAdmin();
183
+
184
+ const { data: profile } = await admin
185
+ .from('profiles')
186
+ .select('role')
187
+ .eq('id', userId)
188
+ .single();
189
+
190
+ if (!profile) return false;
191
+
192
+ // Customize this based on your role hierarchy
193
+ const roleHierarchy: Record<string, number> = {
194
+ viewer: 0,
195
+ editor: 1,
196
+ admin: 2,
197
+ super_admin: 3,
198
+ };
199
+
200
+ const userRoleLevel = roleHierarchy[profile.role] ?? 0;
201
+ const requiredRoleLevel = roleHierarchy[requiredRole] ?? 999;
202
+
203
+ return userRoleLevel >= requiredRoleLevel;
204
+ }
205
+
206
+ // =============================================================================
207
+ // TYPE DEFINITIONS (customize for your database)
208
+ // =============================================================================
209
+
210
+ /**
211
+ * Generate types with:
212
+ * npx supabase gen types typescript --project-id your-project > types/database.ts
213
+ *
214
+ * Then import and use:
215
+ * import { Database } from '@/types/database';
216
+ * const supabase = createClient<Database>(url, key);
217
+ */
218
+
219
+ // Example type for user profiles
220
+ export interface Profile {
221
+ id: string;
222
+ email: string;
223
+ role: 'viewer' | 'editor' | 'admin';
224
+ created_at: string;
225
+ }