ship-safe 1.0.0 → 2.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.
@@ -11,8 +11,11 @@
11
11
  *
12
12
  * MAINTENANCE NOTES:
13
13
  * - Patterns should have low false-positive rates
14
+ * - Only include patterns with SPECIFIC PREFIXES to avoid noise
14
15
  * - Test new patterns against real codebases before adding
15
16
  * - Order doesn't matter (all patterns are checked)
17
+ *
18
+ * v1.2.0 - Added 40+ new patterns for 2025-2026 services
16
19
  */
17
20
 
18
21
  export const SECRET_PATTERNS = [
@@ -49,27 +52,69 @@ export const SECRET_PATTERNS = [
49
52
  severity: 'critical',
50
53
  description: 'GitHub App tokens have installation-level access to repositories.'
51
54
  },
55
+ {
56
+ name: 'GitHub Fine-Grained PAT',
57
+ pattern: /github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}/g,
58
+ severity: 'critical',
59
+ description: 'GitHub fine-grained PATs can access repositories with scoped permissions.'
60
+ },
52
61
  {
53
62
  name: 'Stripe Live Secret Key',
54
63
  pattern: /sk_live_[a-zA-Z0-9]{24,}/g,
55
64
  severity: 'critical',
56
65
  description: 'Stripe live keys can process real payments and access customer data.'
57
66
  },
58
- {
59
- name: 'Stripe Live Publishable Key',
60
- pattern: /pk_live_[a-zA-Z0-9]{24,}/g,
61
- severity: 'high',
62
- description: 'Stripe publishable keys are less sensitive but should not be in server code.'
63
- },
64
67
  {
65
68
  name: 'Private Key Block',
66
69
  pattern: /-----BEGIN\s+(RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/g,
67
70
  severity: 'critical',
68
71
  description: 'Private keys enable impersonation and decryption. Never commit these.'
69
72
  },
73
+ {
74
+ name: 'PlanetScale Password',
75
+ pattern: /pscale_pw_[a-zA-Z0-9_-]{32,}/g,
76
+ severity: 'critical',
77
+ description: 'PlanetScale passwords grant database access. Keep in environment variables.'
78
+ },
79
+ {
80
+ name: 'PlanetScale OAuth Token',
81
+ pattern: /pscale_oauth_[a-zA-Z0-9_-]{32,}/g,
82
+ severity: 'critical',
83
+ description: 'PlanetScale OAuth tokens can manage your database branches and schema.'
84
+ },
85
+ {
86
+ name: 'Clerk Secret Key',
87
+ pattern: /sk_live_[a-zA-Z0-9]{27,}/g,
88
+ severity: 'critical',
89
+ description: 'Clerk secret keys grant full access to your auth system. Never expose in frontend.'
90
+ },
91
+ {
92
+ name: 'Doppler Service Token',
93
+ pattern: /dp\.st\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9]{32,}/g,
94
+ severity: 'critical',
95
+ description: 'Doppler service tokens grant access to your secrets. Ironic if leaked!'
96
+ },
97
+ {
98
+ name: 'HashiCorp Vault Token',
99
+ pattern: /hvs\.[a-zA-Z0-9_-]{24,}/g,
100
+ severity: 'critical',
101
+ description: 'HashiCorp Vault tokens grant access to your secrets.'
102
+ },
103
+ {
104
+ name: 'Neon Database Connection String',
105
+ pattern: /postgres(ql)?:\/\/[^:]+:[^@]+@[^.]+\.neon\.tech/g,
106
+ severity: 'critical',
107
+ description: 'Neon Postgres connection strings contain database credentials.'
108
+ },
109
+ {
110
+ name: 'MongoDB Atlas Connection String',
111
+ pattern: /mongodb(\+srv)?:\/\/[^:]+:[^@]+@[^.]+\.mongodb\.net/g,
112
+ severity: 'critical',
113
+ description: 'MongoDB Atlas connection strings contain database credentials.'
114
+ },
70
115
 
71
116
  // =========================================================================
72
- // HIGH: Very likely to be secrets
117
+ // HIGH: AI/ML Provider Keys (2025-2026)
73
118
  // =========================================================================
74
119
  {
75
120
  name: 'OpenAI API Key',
@@ -77,12 +122,70 @@ export const SECRET_PATTERNS = [
77
122
  severity: 'high',
78
123
  description: 'OpenAI keys can rack up API charges and access your usage history.'
79
124
  },
125
+ {
126
+ name: 'OpenAI Project Key',
127
+ pattern: /sk-proj-[a-zA-Z0-9_-]{48,}/g,
128
+ severity: 'high',
129
+ description: 'OpenAI project keys grant access to specific project resources.'
130
+ },
80
131
  {
81
132
  name: 'Anthropic API Key',
82
- pattern: /sk-ant-[a-zA-Z0-9-]{32,}/g,
133
+ pattern: /sk-ant-[a-zA-Z0-9_-]{32,}/g,
83
134
  severity: 'high',
84
135
  description: 'Anthropic API keys grant access to Claude and your usage quota.'
85
136
  },
137
+ {
138
+ name: 'Google AI (Gemini) API Key',
139
+ pattern: /AIzaSy[a-zA-Z0-9_-]{33}/g,
140
+ severity: 'high',
141
+ description: 'Google AI API keys grant access to Gemini and other Google AI services.'
142
+ },
143
+ {
144
+ name: 'Replicate API Token',
145
+ pattern: /r8_[a-zA-Z0-9]{37}/g,
146
+ severity: 'high',
147
+ description: 'Replicate tokens can run AI models and incur charges on your account.'
148
+ },
149
+ {
150
+ name: 'Hugging Face Token',
151
+ pattern: /hf_[a-zA-Z0-9]{34}/g,
152
+ severity: 'high',
153
+ description: 'Hugging Face tokens grant access to models, datasets, and Inference API.'
154
+ },
155
+ {
156
+ name: 'Perplexity API Key',
157
+ pattern: /pplx-[a-f0-9]{48}/g,
158
+ severity: 'high',
159
+ description: 'Perplexity API keys can access their search-augmented AI models.'
160
+ },
161
+ {
162
+ name: 'Groq API Key',
163
+ pattern: /gsk_[a-zA-Z0-9]{52}/g,
164
+ severity: 'high',
165
+ description: 'Groq API keys provide access to fast LLM inference.'
166
+ },
167
+ {
168
+ name: 'Cohere API Key',
169
+ pattern: /(?:cohere|COHERE)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-zA-Z0-9]{40})["']?/gi,
170
+ severity: 'high',
171
+ description: 'Cohere API keys grant access to their NLP models.'
172
+ },
173
+ {
174
+ name: 'Mistral API Key',
175
+ pattern: /(?:mistral|MISTRAL)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-zA-Z0-9]{32})["']?/gi,
176
+ severity: 'high',
177
+ description: 'Mistral AI API keys can access their language models.'
178
+ },
179
+ {
180
+ name: 'Together AI API Key',
181
+ pattern: /(?:together|TOGETHER)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-f0-9]{64})["']?/gi,
182
+ severity: 'high',
183
+ description: 'Together AI keys grant access to open-source model hosting.'
184
+ },
185
+
186
+ // =========================================================================
187
+ // HIGH: Communication & Messaging
188
+ // =========================================================================
86
189
  {
87
190
  name: 'Slack Token',
88
191
  pattern: /xox[baprs]-[0-9]{10,13}-[0-9]{10,13}[a-zA-Z0-9-]*/g,
@@ -108,11 +211,15 @@ export const SECRET_PATTERNS = [
108
211
  description: 'Discord bot tokens grant full control over your bot.'
109
212
  },
110
213
  {
111
- name: 'Twilio API Key',
112
- pattern: /SK[a-f0-9]{32}/g,
214
+ name: 'Telegram Bot Token',
215
+ pattern: /[0-9]{8,10}:[a-zA-Z0-9_-]{35}/g,
113
216
  severity: 'high',
114
- description: 'Twilio keys can send SMS/calls and access account data.'
217
+ description: 'Telegram bot tokens grant full control over your bot.'
115
218
  },
219
+
220
+ // =========================================================================
221
+ // HIGH: Email Services
222
+ // =========================================================================
116
223
  {
117
224
  name: 'SendGrid API Key',
118
225
  pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/g,
@@ -125,6 +232,44 @@ export const SECRET_PATTERNS = [
125
232
  severity: 'high',
126
233
  description: 'Mailgun keys can send emails and access logs.'
127
234
  },
235
+ {
236
+ name: 'Resend API Key',
237
+ pattern: /re_[a-zA-Z0-9]{32,}/g,
238
+ severity: 'high',
239
+ description: 'Resend API keys can send emails from your account and access logs.'
240
+ },
241
+ {
242
+ name: 'Postmark Server Token',
243
+ pattern: /(?:postmark|POSTMARK)[_-]?(?:server[_-]?)?token["']?\s*[:=]\s*["']?([a-f0-9-]{36})["']?/gi,
244
+ severity: 'high',
245
+ description: 'Postmark tokens can send emails from your account.'
246
+ },
247
+ {
248
+ name: 'Mailchimp API Key',
249
+ pattern: /[a-f0-9]{32}-us[0-9]{1,2}/g,
250
+ severity: 'high',
251
+ description: 'Mailchimp API keys can access your audience and send campaigns.'
252
+ },
253
+
254
+ // =========================================================================
255
+ // HIGH: SMS & Phone
256
+ // =========================================================================
257
+ {
258
+ name: 'Twilio API Key',
259
+ pattern: /SK[a-f0-9]{32}/g,
260
+ severity: 'high',
261
+ description: 'Twilio keys can send SMS/calls and access account data.'
262
+ },
263
+ {
264
+ name: 'Twilio Account SID',
265
+ pattern: /AC[a-f0-9]{32}/g,
266
+ severity: 'medium',
267
+ description: 'Twilio Account SIDs identify your account. Usually paired with auth token.'
268
+ },
269
+
270
+ // =========================================================================
271
+ // HIGH: Databases & Backend Services
272
+ // =========================================================================
128
273
  {
129
274
  name: 'Firebase/Google Service Account',
130
275
  pattern: /"type":\s*"service_account"/g,
@@ -137,6 +282,34 @@ export const SECRET_PATTERNS = [
137
282
  severity: 'high',
138
283
  description: 'Supabase service role keys bypass Row Level Security. Keep server-side only.'
139
284
  },
285
+ {
286
+ name: 'Upstash Redis REST Token',
287
+ pattern: /AX[a-zA-Z0-9]{34,}/g,
288
+ severity: 'high',
289
+ description: 'Upstash Redis tokens grant access to your serverless Redis database.'
290
+ },
291
+ {
292
+ name: 'Upstash QStash Token',
293
+ pattern: /qstash_[a-zA-Z0-9]{32,}/g,
294
+ severity: 'high',
295
+ description: 'Upstash QStash tokens can schedule and manage message queues.'
296
+ },
297
+ {
298
+ name: 'Turso Database URL',
299
+ pattern: /libsql:\/\/[^.]+\.turso\.io/g,
300
+ severity: 'high',
301
+ description: 'Turso database URLs. Check for embedded auth tokens in full connection string.'
302
+ },
303
+ {
304
+ name: 'Convex Deployment URL',
305
+ pattern: /https:\/\/[a-z]+-[a-z]+-[0-9]+\.convex\.cloud/g,
306
+ severity: 'medium',
307
+ description: 'Convex deployment URLs identify your backend. Check for paired secrets.'
308
+ },
309
+
310
+ // =========================================================================
311
+ // HIGH: Hosting & Deployment
312
+ // =========================================================================
140
313
  {
141
314
  name: 'Vercel Token',
142
315
  pattern: /vercel_[a-zA-Z0-9]{24}/gi,
@@ -161,19 +334,167 @@ export const SECRET_PATTERNS = [
161
334
  severity: 'high',
162
335
  description: 'DigitalOcean tokens can manage droplets and resources.'
163
336
  },
337
+ {
338
+ name: 'Render API Key',
339
+ pattern: /rnd_[a-zA-Z0-9]{32,}/g,
340
+ severity: 'high',
341
+ description: 'Render API keys can manage your services and deployments.'
342
+ },
343
+ {
344
+ name: 'Fly.io Token',
345
+ pattern: /FlyV1\s+[a-zA-Z0-9_-]{43}/g,
346
+ severity: 'high',
347
+ description: 'Fly.io tokens can deploy and manage your applications.'
348
+ },
349
+ {
350
+ name: 'Railway Token',
351
+ pattern: /(?:railway|RAILWAY)[_-]?token["']?\s*[:=]\s*["']?([a-f0-9-]{36})["']?/gi,
352
+ severity: 'high',
353
+ description: 'Railway API tokens can manage your services.'
354
+ },
355
+ {
356
+ name: 'Netlify Personal Access Token',
357
+ pattern: /nfp_[a-zA-Z0-9]{40}/g,
358
+ severity: 'high',
359
+ description: 'Netlify PATs can manage sites and deploys.'
360
+ },
361
+ {
362
+ name: 'Cloudflare API Token',
363
+ pattern: /(?:cloudflare|CF)[_-]?(?:api[_-]?)?token["']?\s*[:=]\s*["']?([a-zA-Z0-9_-]{40})["']?/gi,
364
+ severity: 'high',
365
+ description: 'Cloudflare API tokens can manage DNS, workers, and other services.'
366
+ },
164
367
 
165
368
  // =========================================================================
166
- // MEDIUM: Likely secrets, but may have false positives
369
+ // HIGH: Auth Providers
167
370
  // =========================================================================
168
371
  {
169
- name: 'Generic API Key',
170
- pattern: /["']?(?:api[_-]?key|apikey)["']?\s*[:=]\s*["']([a-zA-Z0-9_\-]{16,})["']/gi,
372
+ name: 'Clerk Publishable Key (Live)',
373
+ pattern: /pk_live_[a-zA-Z0-9]{27,}/g,
374
+ severity: 'medium',
375
+ description: 'Clerk publishable keys are meant for frontend but verify it\'s intentional.'
376
+ },
377
+ {
378
+ name: 'Clerk Test Secret Key',
379
+ pattern: /sk_test_[a-zA-Z0-9]{27,}/g,
380
+ severity: 'medium',
381
+ description: 'Clerk test keys are lower risk but should still be in environment variables.'
382
+ },
383
+ {
384
+ name: 'Auth0 Domain with Credentials',
385
+ pattern: /https:\/\/[^.]+\.auth0\.com.*client_secret/gi,
386
+ severity: 'critical',
387
+ description: 'Auth0 URLs with embedded client secrets should never be in code.'
388
+ },
389
+ {
390
+ name: 'Supabase Anon Key in Code',
391
+ pattern: /(?:supabase|SUPABASE)[_-]?(?:anon[_-]?)?key["']?\s*[:=]\s*["']?(eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+)["']?/gi,
392
+ severity: 'medium',
393
+ description: 'Supabase anon keys. Safe for frontend but verify RLS is enabled.'
394
+ },
395
+
396
+ // =========================================================================
397
+ // HIGH: Productivity & SaaS
398
+ // =========================================================================
399
+ {
400
+ name: 'Linear API Key',
401
+ pattern: /lin_api_[a-zA-Z0-9]{40}/g,
402
+ severity: 'high',
403
+ description: 'Linear API keys can access your project management data.'
404
+ },
405
+ {
406
+ name: 'Notion API Key',
407
+ pattern: /secret_[a-zA-Z0-9]{43}/g,
408
+ severity: 'high',
409
+ description: 'Notion API keys can access and modify your workspace content.'
410
+ },
411
+ {
412
+ name: 'Airtable API Key',
413
+ pattern: /pat[a-zA-Z0-9]{14}\.[a-f0-9]{64}/g,
414
+ severity: 'high',
415
+ description: 'Airtable personal access tokens grant access to your bases.'
416
+ },
417
+ {
418
+ name: 'Figma Personal Access Token',
419
+ pattern: /figd_[a-zA-Z0-9_-]{40,}/g,
420
+ severity: 'high',
421
+ description: 'Figma PATs can access your design files and projects.'
422
+ },
423
+
424
+ // =========================================================================
425
+ // HIGH: Payments (Additional)
426
+ // =========================================================================
427
+ {
428
+ name: 'Stripe Test Secret Key',
429
+ pattern: /sk_test_[a-zA-Z0-9]{24,}/g,
430
+ severity: 'medium',
431
+ description: 'Stripe test keys are lower risk but should still be in environment variables.'
432
+ },
433
+ {
434
+ name: 'Stripe Live Publishable Key',
435
+ pattern: /pk_live_[a-zA-Z0-9]{24,}/g,
436
+ severity: 'medium',
437
+ description: 'Stripe publishable keys are meant for frontend but verify it\'s intentional.'
438
+ },
439
+ {
440
+ name: 'Stripe Webhook Secret',
441
+ pattern: /whsec_[a-zA-Z0-9]{32,}/g,
442
+ severity: 'high',
443
+ description: 'Stripe webhook secrets validate incoming webhooks. Keep server-side only.'
444
+ },
445
+ {
446
+ name: 'Lemon Squeezy API Key',
447
+ pattern: /(?:lemon|LEMON)[_-]?(?:squeezy|SQUEEZY)?[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-f0-9-]{36})["']?/gi,
448
+ severity: 'high',
449
+ description: 'Lemon Squeezy API keys can manage your store and orders.'
450
+ },
451
+ {
452
+ name: 'Paddle API Key',
453
+ pattern: /(?:paddle|PADDLE)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-f0-9]{64})["']?/gi,
454
+ severity: 'high',
455
+ description: 'Paddle API keys can manage your subscriptions and payments.'
456
+ },
457
+
458
+ // =========================================================================
459
+ // HIGH: Analytics & Monitoring
460
+ // =========================================================================
461
+ {
462
+ name: 'Sentry DSN',
463
+ pattern: /https:\/\/[a-f0-9]{32}@[a-z0-9]+\.ingest\.sentry\.io\/[0-9]+/g,
464
+ severity: 'medium',
465
+ description: 'Sentry DSNs are semi-public but contain project identifiers.'
466
+ },
467
+ {
468
+ name: 'PostHog API Key',
469
+ pattern: /phc_[a-zA-Z0-9]{32,}/g,
470
+ severity: 'medium',
471
+ description: 'PostHog project API keys. Usually safe in frontend but verify.'
472
+ },
473
+ {
474
+ name: 'New Relic API Key',
475
+ pattern: /NRAK-[A-Z0-9]{27}/g,
476
+ severity: 'high',
477
+ description: 'New Relic API keys can access your monitoring data and configurations.'
478
+ },
479
+ {
480
+ name: 'Datadog API Key',
481
+ pattern: /(?:datadog|DD)[_-]?(?:api[_-]?)?key["']?\s*[:=]\s*["']?([a-f0-9]{32})["']?/gi,
482
+ severity: 'high',
483
+ description: 'Datadog API keys can access and send monitoring data.'
484
+ },
485
+
486
+ // =========================================================================
487
+ // MEDIUM: Generic patterns (may have false positives)
488
+ // =========================================================================
489
+ {
490
+ name: 'Generic API Key Assignment',
491
+ pattern: /["']?(?:api[_-]?key|apikey)["']?\s*[:=]\s*["']([a-zA-Z0-9_\-]{20,})["']/gi,
171
492
  severity: 'medium',
172
493
  description: 'Hardcoded API keys should be moved to environment variables.'
173
494
  },
174
495
  {
175
- name: 'Generic Secret',
176
- pattern: /["']?(?:secret|secret[_-]?key)["']?\s*[:=]\s*["']([a-zA-Z0-9_\-]{16,})["']/gi,
496
+ name: 'Generic Secret Assignment',
497
+ pattern: /["']?(?:secret|secret[_-]?key)["']?\s*[:=]\s*["']([a-zA-Z0-9_\-]{20,})["']/gi,
177
498
  severity: 'medium',
178
499
  description: 'Hardcoded secrets should be moved to environment variables.'
179
500
  },
@@ -190,22 +511,22 @@ export const SECRET_PATTERNS = [
190
511
  description: 'Database URLs with embedded passwords expose your database.'
191
512
  },
192
513
  {
193
- name: 'Bearer Token',
514
+ name: 'Bearer Token in Code',
194
515
  pattern: /["']Bearer\s+[a-zA-Z0-9_\-\.=]{20,}["']/gi,
195
516
  severity: 'medium',
196
517
  description: 'Hardcoded bearer tokens should not be in source code.'
197
518
  },
198
- {
199
- name: 'JWT Token',
200
- pattern: /["']?jwt["']?\s*[:=]\s*["']?(eyJ[a-zA-Z0-9_-]*\.eyJ[a-zA-Z0-9_-]*\.[a-zA-Z0-9_-]*)["']?/gi,
201
- severity: 'medium',
202
- description: 'JWTs in source code may be test tokens, but verify they\'re not production.'
203
- },
204
519
  {
205
520
  name: 'Basic Auth Header',
206
- pattern: /["']Basic\s+[A-Za-z0-9+/=]{10,}["']/gi,
521
+ pattern: /["']Basic\s+[A-Za-z0-9+/=]{20,}["']/gi,
207
522
  severity: 'medium',
208
523
  description: 'Basic auth headers contain base64-encoded credentials.'
524
+ },
525
+ {
526
+ name: 'Private Key in Environment Variable',
527
+ pattern: /PRIVATE[_-]?KEY["']?\s*[:=]\s*["']([^"']+)["']/gi,
528
+ severity: 'high',
529
+ description: 'Private keys should be loaded from files, not hardcoded.'
209
530
  }
210
531
  ];
211
532
 
@@ -0,0 +1,215 @@
1
+ // =============================================================================
2
+ // FIREBASE FIRESTORE SECURITY RULES TEMPLATES
3
+ // =============================================================================
4
+ //
5
+ // Copy these rules to your Firebase Console > Firestore > Rules
6
+ //
7
+ // WHY THIS MATTERS:
8
+ // - Default test mode rules allow ANYONE to read/write ALL data
9
+ // - Test mode is the #1 cause of Firebase data breaches
10
+ // - 40% of Firebase apps have misconfigured security rules
11
+ //
12
+ // HOW TO USE:
13
+ // 1. Go to Firebase Console > Firestore Database > Rules
14
+ // 2. Replace the default rules with these templates
15
+ // 3. Customize for your data structure
16
+ // 4. Publish and test thoroughly
17
+ //
18
+ // =============================================================================
19
+
20
+ rules_version = '2';
21
+ service cloud.firestore {
22
+ match /databases/{database}/documents {
23
+
24
+ // =========================================================================
25
+ // HELPER FUNCTIONS
26
+ // =========================================================================
27
+
28
+ // Check if user is authenticated
29
+ function isAuthenticated() {
30
+ return request.auth != null;
31
+ }
32
+
33
+ // Get the authenticated user's ID
34
+ function userId() {
35
+ return request.auth.uid;
36
+ }
37
+
38
+ // Check if user owns a document (document has a userId field)
39
+ function isOwner() {
40
+ return isAuthenticated() && resource.data.userId == userId();
41
+ }
42
+
43
+ // Check if user is creating a document with their own userId
44
+ function isCreatingOwn() {
45
+ return isAuthenticated() && request.resource.data.userId == userId();
46
+ }
47
+
48
+ // Check if user has a specific role (requires a users collection with role field)
49
+ function hasRole(role) {
50
+ return isAuthenticated() &&
51
+ get(/databases/$(database)/documents/users/$(userId())).data.role == role;
52
+ }
53
+
54
+ // Check if user is admin
55
+ function isAdmin() {
56
+ return hasRole('admin');
57
+ }
58
+
59
+ // Validate that required fields exist
60
+ function hasRequiredFields(fields) {
61
+ return request.resource.data.keys().hasAll(fields);
62
+ }
63
+
64
+ // Check if only allowed fields are being modified
65
+ function onlyUpdating(fields) {
66
+ return request.resource.data.diff(resource.data).affectedKeys().hasOnly(fields);
67
+ }
68
+
69
+ // =========================================================================
70
+ // PATTERN 1: USER PROFILES (User owns their profile)
71
+ // =========================================================================
72
+
73
+ match /users/{userId} {
74
+ // Users can read their own profile
75
+ allow read: if isAuthenticated() && request.auth.uid == userId;
76
+
77
+ // Users can create their own profile
78
+ allow create: if isAuthenticated()
79
+ && request.auth.uid == userId
80
+ && hasRequiredFields(['email', 'createdAt'])
81
+ && request.resource.data.createdAt == request.time;
82
+
83
+ // Users can update their own profile (except role)
84
+ allow update: if isAuthenticated()
85
+ && request.auth.uid == userId
86
+ && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role']);
87
+
88
+ // Only admins can delete users
89
+ allow delete: if isAdmin();
90
+ }
91
+
92
+ // =========================================================================
93
+ // PATTERN 2: PRIVATE DATA (User's own data)
94
+ // =========================================================================
95
+
96
+ match /private/{userId}/{document=**} {
97
+ // Users can only access their own private data
98
+ allow read, write: if isAuthenticated() && request.auth.uid == userId;
99
+ }
100
+
101
+ // =========================================================================
102
+ // PATTERN 3: POSTS/CONTENT (Public read, authenticated write)
103
+ // =========================================================================
104
+
105
+ match /posts/{postId} {
106
+ // Anyone can read published posts
107
+ allow read: if resource.data.published == true
108
+ || (isAuthenticated() && resource.data.authorId == userId());
109
+
110
+ // Authenticated users can create posts
111
+ allow create: if isAuthenticated()
112
+ && request.resource.data.authorId == userId()
113
+ && hasRequiredFields(['title', 'content', 'authorId', 'createdAt'])
114
+ && request.resource.data.createdAt == request.time;
115
+
116
+ // Authors can update their own posts
117
+ allow update: if isAuthenticated()
118
+ && resource.data.authorId == userId()
119
+ && request.resource.data.authorId == userId(); // Can't change author
120
+
121
+ // Authors and admins can delete
122
+ allow delete: if isAuthenticated()
123
+ && (resource.data.authorId == userId() || isAdmin());
124
+ }
125
+
126
+ // =========================================================================
127
+ // PATTERN 4: ORGANIZATION/TEAM DATA
128
+ // =========================================================================
129
+
130
+ match /organizations/{orgId} {
131
+ // Function to check org membership
132
+ function isOrgMember() {
133
+ return isAuthenticated() &&
134
+ exists(/databases/$(database)/documents/organizations/$(orgId)/members/$(userId()));
135
+ }
136
+
137
+ function isOrgAdmin() {
138
+ return isAuthenticated() &&
139
+ get(/databases/$(database)/documents/organizations/$(orgId)/members/$(userId())).data.role == 'admin';
140
+ }
141
+
142
+ // Org members can read org data
143
+ allow read: if isOrgMember();
144
+
145
+ // Only org admins can update org settings
146
+ allow update: if isOrgAdmin();
147
+
148
+ // Nested collection: org members
149
+ match /members/{memberId} {
150
+ allow read: if isOrgMember();
151
+ allow write: if isOrgAdmin();
152
+ }
153
+
154
+ // Nested collection: org projects
155
+ match /projects/{projectId} {
156
+ allow read: if isOrgMember();
157
+ allow create: if isOrgMember()
158
+ && request.resource.data.createdBy == userId();
159
+ allow update: if isOrgMember();
160
+ allow delete: if isOrgAdmin();
161
+ }
162
+ }
163
+
164
+ // =========================================================================
165
+ // PATTERN 5: RATE LIMITING (Using timestamps)
166
+ // =========================================================================
167
+
168
+ match /rateLimited/{docId} {
169
+ // Allow creation only if user hasn't created one in the last minute
170
+ allow create: if isAuthenticated()
171
+ && (!exists(/databases/$(database)/documents/rateLimited/$(userId()))
172
+ || get(/databases/$(database)/documents/rateLimited/$(userId())).data.lastAction < request.time - duration.value(1, 'm'));
173
+ }
174
+
175
+ // =========================================================================
176
+ // PATTERN 6: READ-ONLY PUBLIC DATA
177
+ // =========================================================================
178
+
179
+ match /public/{document=**} {
180
+ // Anyone can read public data
181
+ allow read: if true;
182
+
183
+ // Only admins can write
184
+ allow write: if isAdmin();
185
+ }
186
+
187
+ // =========================================================================
188
+ // ANTI-PATTERNS: NEVER DO THIS
189
+ // =========================================================================
190
+
191
+ // BAD: Allows anyone to read/write everything
192
+ // match /{document=**} {
193
+ // allow read, write: if true;
194
+ // }
195
+
196
+ // BAD: Test mode rules (auto-generated, REMOVE before production)
197
+ // match /{document=**} {
198
+ // allow read, write: if request.time < timestamp.date(2024, 12, 31);
199
+ // }
200
+
201
+ // BAD: Checking auth but not ownership
202
+ // match /users/{userId} {
203
+ // allow read, write: if request.auth != null; // Any user can access any user!
204
+ // }
205
+
206
+ // =========================================================================
207
+ // DEFAULT: DENY ALL (Safety net)
208
+ // =========================================================================
209
+
210
+ // Deny access to any collection not explicitly defined above
211
+ match /{document=**} {
212
+ allow read, write: if false;
213
+ }
214
+ }
215
+ }