create-react-native-airborne 0.0.1

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.
Files changed (78) hide show
  1. package/README.md +24 -0
  2. package/package.json +21 -0
  3. package/src/index.mjs +103 -0
  4. package/template/.agents/skills/convex-best-practices/SKILL.md +333 -0
  5. package/template/.agents/skills/convex-file-storage/SKILL.md +466 -0
  6. package/template/.agents/skills/convex-security-audit/SKILL.md +538 -0
  7. package/template/.agents/skills/convex-security-check/SKILL.md +377 -0
  8. package/template/.github/workflows/ci.yml +130 -0
  9. package/template/.prettierignore +8 -0
  10. package/template/.prettierrc.json +6 -0
  11. package/template/AGENTS.md +156 -0
  12. package/template/Justfile +48 -0
  13. package/template/README.md +94 -0
  14. package/template/client/.env.example +3 -0
  15. package/template/client/.vscode/extensions.json +1 -0
  16. package/template/client/.vscode/settings.json +7 -0
  17. package/template/client/README.md +33 -0
  18. package/template/client/app/(app)/_layout.tsx +34 -0
  19. package/template/client/app/(app)/index.tsx +66 -0
  20. package/template/client/app/(app)/push.tsx +75 -0
  21. package/template/client/app/(app)/settings.tsx +36 -0
  22. package/template/client/app/(auth)/_layout.tsx +22 -0
  23. package/template/client/app/(auth)/sign-in.tsx +358 -0
  24. package/template/client/app/(auth)/sign-up.tsx +237 -0
  25. package/template/client/app/_layout.tsx +30 -0
  26. package/template/client/app/index.tsx +127 -0
  27. package/template/client/app.config.ts +30 -0
  28. package/template/client/assets/images/android-icon-background.png +0 -0
  29. package/template/client/assets/images/android-icon-foreground.png +0 -0
  30. package/template/client/assets/images/android-icon-monochrome.png +0 -0
  31. package/template/client/assets/images/favicon.png +0 -0
  32. package/template/client/assets/images/icon.png +0 -0
  33. package/template/client/assets/images/partial-react-logo.png +0 -0
  34. package/template/client/assets/images/react-logo.png +0 -0
  35. package/template/client/assets/images/react-logo@2x.png +0 -0
  36. package/template/client/assets/images/react-logo@3x.png +0 -0
  37. package/template/client/assets/images/splash-icon.png +0 -0
  38. package/template/client/eslint.config.js +10 -0
  39. package/template/client/global.css +2 -0
  40. package/template/client/metro.config.js +9 -0
  41. package/template/client/package.json +51 -0
  42. package/template/client/src/components/auth-shell.tsx +63 -0
  43. package/template/client/src/components/form-input.tsx +62 -0
  44. package/template/client/src/components/primary-button.tsx +37 -0
  45. package/template/client/src/components/screen.tsx +17 -0
  46. package/template/client/src/components/sign-out-button.tsx +32 -0
  47. package/template/client/src/hooks/use-theme-sync.ts +11 -0
  48. package/template/client/src/lib/convex.ts +6 -0
  49. package/template/client/src/lib/env-schema.ts +13 -0
  50. package/template/client/src/lib/env.test.ts +24 -0
  51. package/template/client/src/lib/env.ts +19 -0
  52. package/template/client/src/lib/notifications.ts +47 -0
  53. package/template/client/src/store/preferences-store.ts +42 -0
  54. package/template/client/src/types/theme.ts +1 -0
  55. package/template/client/tsconfig.json +18 -0
  56. package/template/client/uniwind-types.d.ts +10 -0
  57. package/template/client/vitest.config.ts +7 -0
  58. package/template/package.json +22 -0
  59. package/template/server/.env.example +8 -0
  60. package/template/server/README.md +31 -0
  61. package/template/server/convex/_generated/api.d.ts +55 -0
  62. package/template/server/convex/_generated/api.js +23 -0
  63. package/template/server/convex/_generated/dataModel.d.ts +60 -0
  64. package/template/server/convex/_generated/server.d.ts +143 -0
  65. package/template/server/convex/_generated/server.js +93 -0
  66. package/template/server/convex/auth.config.ts +11 -0
  67. package/template/server/convex/env.ts +18 -0
  68. package/template/server/convex/lib.ts +12 -0
  69. package/template/server/convex/push.ts +148 -0
  70. package/template/server/convex/schema.ts +22 -0
  71. package/template/server/convex/users.ts +54 -0
  72. package/template/server/convex.json +3 -0
  73. package/template/server/eslint.config.js +51 -0
  74. package/template/server/package.json +29 -0
  75. package/template/server/tests/convex.test.ts +52 -0
  76. package/template/server/tests/import-meta.d.ts +3 -0
  77. package/template/server/tsconfig.json +15 -0
  78. package/template/server/vitest.config.ts +13 -0
@@ -0,0 +1,538 @@
1
+ ---
2
+ name: Convex Security Audit
3
+ description: Deep security review patterns for authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operations
4
+ version: 1.0.0
5
+ author: Convex
6
+ tags: [convex, security, audit, authorization, rate-limiting, protection]
7
+ ---
8
+
9
+ # Convex Security Audit
10
+
11
+ Comprehensive security review patterns for Convex applications including authorization logic, data access boundaries, action isolation, rate limiting, and protecting sensitive operations.
12
+
13
+ ## Documentation Sources
14
+
15
+ Before implementing, do not assume; fetch the latest documentation:
16
+
17
+ - Primary: https://docs.convex.dev/auth/functions-auth
18
+ - Production Security: https://docs.convex.dev/production
19
+ - For broader context: https://docs.convex.dev/llms.txt
20
+
21
+ ## Instructions
22
+
23
+ ### Security Audit Areas
24
+
25
+ 1. **Authorization Logic** - Who can do what
26
+ 2. **Data Access Boundaries** - What data users can see
27
+ 3. **Action Isolation** - Protecting external API calls
28
+ 4. **Rate Limiting** - Preventing abuse
29
+ 5. **Sensitive Operations** - Protecting critical functions
30
+
31
+ ### Authorization Logic Audit
32
+
33
+ #### Role-Based Access Control (RBAC)
34
+
35
+ ```typescript
36
+ // convex/lib/auth.ts
37
+ import { QueryCtx, MutationCtx } from "./_generated/server";
38
+ import { ConvexError } from "convex/values";
39
+ import { Doc } from "./_generated/dataModel";
40
+
41
+ type UserRole = "user" | "moderator" | "admin" | "superadmin";
42
+
43
+ const roleHierarchy: Record<UserRole, number> = {
44
+ user: 0,
45
+ moderator: 1,
46
+ admin: 2,
47
+ superadmin: 3,
48
+ };
49
+
50
+ export async function getUser(ctx: QueryCtx | MutationCtx): Promise<Doc<"users"> | null> {
51
+ const identity = await ctx.auth.getUserIdentity();
52
+ if (!identity) return null;
53
+
54
+ return await ctx.db
55
+ .query("users")
56
+ .withIndex("by_tokenIdentifier", (q) =>
57
+ q.eq("tokenIdentifier", identity.tokenIdentifier)
58
+ )
59
+ .unique();
60
+ }
61
+
62
+ export async function requireRole(
63
+ ctx: QueryCtx | MutationCtx,
64
+ minRole: UserRole
65
+ ): Promise<Doc<"users">> {
66
+ const user = await getUser(ctx);
67
+
68
+ if (!user) {
69
+ throw new ConvexError({
70
+ code: "UNAUTHENTICATED",
71
+ message: "Authentication required",
72
+ });
73
+ }
74
+
75
+ const userRoleLevel = roleHierarchy[user.role as UserRole] ?? 0;
76
+ const requiredLevel = roleHierarchy[minRole];
77
+
78
+ if (userRoleLevel < requiredLevel) {
79
+ throw new ConvexError({
80
+ code: "FORBIDDEN",
81
+ message: `Role '${minRole}' or higher required`,
82
+ });
83
+ }
84
+
85
+ return user;
86
+ }
87
+
88
+ // Permission-based check
89
+ type Permission = "read:users" | "write:users" | "delete:users" | "admin:system";
90
+
91
+ const rolePermissions: Record<UserRole, Permission[]> = {
92
+ user: ["read:users"],
93
+ moderator: ["read:users", "write:users"],
94
+ admin: ["read:users", "write:users", "delete:users"],
95
+ superadmin: ["read:users", "write:users", "delete:users", "admin:system"],
96
+ };
97
+
98
+ export async function requirePermission(
99
+ ctx: QueryCtx | MutationCtx,
100
+ permission: Permission
101
+ ): Promise<Doc<"users">> {
102
+ const user = await getUser(ctx);
103
+
104
+ if (!user) {
105
+ throw new ConvexError({ code: "UNAUTHENTICATED", message: "Authentication required" });
106
+ }
107
+
108
+ const userRole = user.role as UserRole;
109
+ const permissions = rolePermissions[userRole] ?? [];
110
+
111
+ if (!permissions.includes(permission)) {
112
+ throw new ConvexError({
113
+ code: "FORBIDDEN",
114
+ message: `Permission '${permission}' required`,
115
+ });
116
+ }
117
+
118
+ return user;
119
+ }
120
+ ```
121
+
122
+ ### Data Access Boundaries Audit
123
+
124
+ ```typescript
125
+ // convex/data.ts
126
+ import { query, mutation } from "./_generated/server";
127
+ import { v } from "convex/values";
128
+ import { getUser, requireRole } from "./lib/auth";
129
+ import { ConvexError } from "convex/values";
130
+
131
+ // Audit: Users can only see their own data
132
+ export const getMyData = query({
133
+ args: {},
134
+ returns: v.array(v.object({
135
+ _id: v.id("userData"),
136
+ content: v.string(),
137
+ })),
138
+ handler: async (ctx) => {
139
+ const user = await getUser(ctx);
140
+ if (!user) return [];
141
+
142
+ // SECURITY: Filter by userId
143
+ return await ctx.db
144
+ .query("userData")
145
+ .withIndex("by_user", (q) => q.eq("userId", user._id))
146
+ .collect();
147
+ },
148
+ });
149
+
150
+ // Audit: Verify ownership before returning sensitive data
151
+ export const getSensitiveItem = query({
152
+ args: { itemId: v.id("sensitiveItems") },
153
+ returns: v.union(v.object({
154
+ _id: v.id("sensitiveItems"),
155
+ secret: v.string(),
156
+ }), v.null()),
157
+ handler: async (ctx, args) => {
158
+ const user = await getUser(ctx);
159
+ if (!user) return null;
160
+
161
+ const item = await ctx.db.get(args.itemId);
162
+
163
+ // SECURITY: Verify ownership
164
+ if (!item || item.ownerId !== user._id) {
165
+ return null; // Don't reveal if item exists
166
+ }
167
+
168
+ return item;
169
+ },
170
+ });
171
+
172
+ // Audit: Shared resources with access list
173
+ export const getSharedDocument = query({
174
+ args: { docId: v.id("documents") },
175
+ returns: v.union(v.object({
176
+ _id: v.id("documents"),
177
+ content: v.string(),
178
+ accessLevel: v.string(),
179
+ }), v.null()),
180
+ handler: async (ctx, args) => {
181
+ const user = await getUser(ctx);
182
+ const doc = await ctx.db.get(args.docId);
183
+
184
+ if (!doc) return null;
185
+
186
+ // Public documents
187
+ if (doc.visibility === "public") {
188
+ return { ...doc, accessLevel: "public" };
189
+ }
190
+
191
+ // Must be authenticated for non-public
192
+ if (!user) return null;
193
+
194
+ // Owner has full access
195
+ if (doc.ownerId === user._id) {
196
+ return { ...doc, accessLevel: "owner" };
197
+ }
198
+
199
+ // Check shared access
200
+ const access = await ctx.db
201
+ .query("documentAccess")
202
+ .withIndex("by_doc_and_user", (q) =>
203
+ q.eq("documentId", args.docId).eq("userId", user._id)
204
+ )
205
+ .unique();
206
+
207
+ if (!access) return null;
208
+
209
+ return { ...doc, accessLevel: access.level };
210
+ },
211
+ });
212
+ ```
213
+
214
+ ### Action Isolation Audit
215
+
216
+ ```typescript
217
+ // convex/actions.ts
218
+ "use node";
219
+
220
+ import { action, internalAction } from "./_generated/server";
221
+ import { v } from "convex/values";
222
+ import { api, internal } from "./_generated/api";
223
+ import { ConvexError } from "convex/values";
224
+
225
+ // SECURITY: Never expose API keys in responses
226
+ export const callExternalAPI = action({
227
+ args: { query: v.string() },
228
+ returns: v.object({ result: v.string() }),
229
+ handler: async (ctx, args) => {
230
+ // Verify user is authenticated
231
+ const identity = await ctx.auth.getUserIdentity();
232
+ if (!identity) {
233
+ throw new ConvexError("Authentication required");
234
+ }
235
+
236
+ // Get API key from environment (not hardcoded)
237
+ const apiKey = process.env.EXTERNAL_API_KEY;
238
+ if (!apiKey) {
239
+ throw new Error("API key not configured");
240
+ }
241
+
242
+ // Log usage for audit trail
243
+ await ctx.runMutation(internal.audit.logAPICall, {
244
+ userId: identity.tokenIdentifier,
245
+ endpoint: "external-api",
246
+ timestamp: Date.now(),
247
+ });
248
+
249
+ const response = await fetch("https://api.example.com/query", {
250
+ method: "POST",
251
+ headers: {
252
+ "Authorization": `Bearer ${apiKey}`,
253
+ "Content-Type": "application/json",
254
+ },
255
+ body: JSON.stringify({ query: args.query }),
256
+ });
257
+
258
+ if (!response.ok) {
259
+ // Don't expose external API error details
260
+ throw new ConvexError("External service unavailable");
261
+ }
262
+
263
+ const data = await response.json();
264
+
265
+ // Sanitize response before returning
266
+ return { result: sanitizeResponse(data) };
267
+ },
268
+ });
269
+
270
+ // Internal action - not exposed to clients
271
+ export const _processPayment = internalAction({
272
+ args: {
273
+ userId: v.id("users"),
274
+ amount: v.number(),
275
+ paymentMethodId: v.string(),
276
+ },
277
+ returns: v.object({ success: v.boolean(), transactionId: v.optional(v.string()) }),
278
+ handler: async (ctx, args) => {
279
+ const stripeKey = process.env.STRIPE_SECRET_KEY;
280
+
281
+ // Process payment with Stripe
282
+ // This should NEVER be exposed as a public action
283
+
284
+ return { success: true, transactionId: "txn_xxx" };
285
+ },
286
+ });
287
+ ```
288
+
289
+ ### Rate Limiting Audit
290
+
291
+ ```typescript
292
+ // convex/rateLimit.ts
293
+ import { mutation, query } from "./_generated/server";
294
+ import { v } from "convex/values";
295
+ import { ConvexError } from "convex/values";
296
+
297
+ const RATE_LIMITS = {
298
+ message: { requests: 10, windowMs: 60000 }, // 10 per minute
299
+ upload: { requests: 5, windowMs: 300000 }, // 5 per 5 minutes
300
+ api: { requests: 100, windowMs: 3600000 }, // 100 per hour
301
+ };
302
+
303
+ export const checkRateLimit = mutation({
304
+ args: {
305
+ userId: v.string(),
306
+ action: v.union(v.literal("message"), v.literal("upload"), v.literal("api")),
307
+ },
308
+ returns: v.object({ allowed: v.boolean(), retryAfter: v.optional(v.number()) }),
309
+ handler: async (ctx, args) => {
310
+ const limit = RATE_LIMITS[args.action];
311
+ const now = Date.now();
312
+ const windowStart = now - limit.windowMs;
313
+
314
+ // Count requests in window
315
+ const requests = await ctx.db
316
+ .query("rateLimits")
317
+ .withIndex("by_user_and_action", (q) =>
318
+ q.eq("userId", args.userId).eq("action", args.action)
319
+ )
320
+ .filter((q) => q.gt(q.field("timestamp"), windowStart))
321
+ .collect();
322
+
323
+ if (requests.length >= limit.requests) {
324
+ const oldestRequest = requests[0];
325
+ const retryAfter = oldestRequest.timestamp + limit.windowMs - now;
326
+
327
+ return { allowed: false, retryAfter };
328
+ }
329
+
330
+ // Record this request
331
+ await ctx.db.insert("rateLimits", {
332
+ userId: args.userId,
333
+ action: args.action,
334
+ timestamp: now,
335
+ });
336
+
337
+ return { allowed: true };
338
+ },
339
+ });
340
+
341
+ // Use in mutations
342
+ export const sendMessage = mutation({
343
+ args: { content: v.string() },
344
+ returns: v.id("messages"),
345
+ handler: async (ctx, args) => {
346
+ const identity = await ctx.auth.getUserIdentity();
347
+ if (!identity) throw new ConvexError("Authentication required");
348
+
349
+ // Check rate limit
350
+ const rateCheck = await checkRateLimit(ctx, {
351
+ userId: identity.tokenIdentifier,
352
+ action: "message",
353
+ });
354
+
355
+ if (!rateCheck.allowed) {
356
+ throw new ConvexError({
357
+ code: "RATE_LIMITED",
358
+ message: `Too many requests. Try again in ${Math.ceil(rateCheck.retryAfter! / 1000)} seconds`,
359
+ });
360
+ }
361
+
362
+ return await ctx.db.insert("messages", {
363
+ content: args.content,
364
+ authorId: identity.tokenIdentifier,
365
+ createdAt: Date.now(),
366
+ });
367
+ },
368
+ });
369
+ ```
370
+
371
+ ### Sensitive Operations Protection
372
+
373
+ ```typescript
374
+ // convex/admin.ts
375
+ import { mutation, internalMutation } from "./_generated/server";
376
+ import { v } from "convex/values";
377
+ import { requireRole, requirePermission } from "./lib/auth";
378
+ import { internal } from "./_generated/api";
379
+
380
+ // Two-factor confirmation for dangerous operations
381
+ export const deleteAllUserData = mutation({
382
+ args: {
383
+ userId: v.id("users"),
384
+ confirmationCode: v.string(),
385
+ },
386
+ returns: v.null(),
387
+ handler: async (ctx, args) => {
388
+ // Require superadmin
389
+ const admin = await requireRole(ctx, "superadmin");
390
+
391
+ // Verify confirmation code
392
+ const confirmation = await ctx.db
393
+ .query("confirmations")
394
+ .withIndex("by_admin_and_code", (q) =>
395
+ q.eq("adminId", admin._id).eq("code", args.confirmationCode)
396
+ )
397
+ .filter((q) => q.gt(q.field("expiresAt"), Date.now()))
398
+ .unique();
399
+
400
+ if (!confirmation || confirmation.action !== "delete_user_data") {
401
+ throw new ConvexError("Invalid or expired confirmation code");
402
+ }
403
+
404
+ // Delete confirmation to prevent reuse
405
+ await ctx.db.delete(confirmation._id);
406
+
407
+ // Schedule deletion (don't do it inline)
408
+ await ctx.scheduler.runAfter(0, internal.admin._performDeletion, {
409
+ userId: args.userId,
410
+ requestedBy: admin._id,
411
+ });
412
+
413
+ // Audit log
414
+ await ctx.db.insert("auditLogs", {
415
+ action: "delete_user_data",
416
+ targetUserId: args.userId,
417
+ performedBy: admin._id,
418
+ timestamp: Date.now(),
419
+ });
420
+
421
+ return null;
422
+ },
423
+ });
424
+
425
+ // Generate confirmation code for sensitive action
426
+ export const requestDeletionConfirmation = mutation({
427
+ args: { userId: v.id("users") },
428
+ returns: v.string(),
429
+ handler: async (ctx, args) => {
430
+ const admin = await requireRole(ctx, "superadmin");
431
+
432
+ const code = generateSecureCode();
433
+
434
+ await ctx.db.insert("confirmations", {
435
+ adminId: admin._id,
436
+ code,
437
+ action: "delete_user_data",
438
+ targetUserId: args.userId,
439
+ expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
440
+ });
441
+
442
+ // In production, send code via secure channel (email, SMS)
443
+ return code;
444
+ },
445
+ });
446
+ ```
447
+
448
+ ## Examples
449
+
450
+ ### Complete Audit Trail System
451
+
452
+ ```typescript
453
+ // convex/audit.ts
454
+ import { mutation, query, internalMutation } from "./_generated/server";
455
+ import { v } from "convex/values";
456
+ import { getUser, requireRole } from "./lib/auth";
457
+
458
+ const auditEventValidator = v.object({
459
+ _id: v.id("auditLogs"),
460
+ _creationTime: v.number(),
461
+ action: v.string(),
462
+ userId: v.optional(v.string()),
463
+ resourceType: v.string(),
464
+ resourceId: v.string(),
465
+ details: v.optional(v.any()),
466
+ ipAddress: v.optional(v.string()),
467
+ timestamp: v.number(),
468
+ });
469
+
470
+ // Internal: Log audit event
471
+ export const logEvent = internalMutation({
472
+ args: {
473
+ action: v.string(),
474
+ userId: v.optional(v.string()),
475
+ resourceType: v.string(),
476
+ resourceId: v.string(),
477
+ details: v.optional(v.any()),
478
+ },
479
+ returns: v.id("auditLogs"),
480
+ handler: async (ctx, args) => {
481
+ return await ctx.db.insert("auditLogs", {
482
+ ...args,
483
+ timestamp: Date.now(),
484
+ });
485
+ },
486
+ });
487
+
488
+ // Admin: View audit logs
489
+ export const getAuditLogs = query({
490
+ args: {
491
+ resourceType: v.optional(v.string()),
492
+ userId: v.optional(v.string()),
493
+ limit: v.optional(v.number()),
494
+ },
495
+ returns: v.array(auditEventValidator),
496
+ handler: async (ctx, args) => {
497
+ await requireRole(ctx, "admin");
498
+
499
+ let query = ctx.db.query("auditLogs");
500
+
501
+ if (args.resourceType) {
502
+ query = query.withIndex("by_resource_type", (q) =>
503
+ q.eq("resourceType", args.resourceType)
504
+ );
505
+ }
506
+
507
+ return await query
508
+ .order("desc")
509
+ .take(args.limit ?? 100);
510
+ },
511
+ });
512
+ ```
513
+
514
+ ## Best Practices
515
+
516
+ - Never run `npx convex deploy` unless explicitly instructed
517
+ - Never run any git commands unless explicitly instructed
518
+ - Implement defense in depth (multiple security layers)
519
+ - Log all sensitive operations for audit trails
520
+ - Use confirmation codes for destructive actions
521
+ - Rate limit all user-facing endpoints
522
+ - Never expose internal API keys or errors
523
+ - Review access patterns regularly
524
+
525
+ ## Common Pitfalls
526
+
527
+ 1. **Single point of failure** - Implement multiple auth checks
528
+ 2. **Missing audit logs** - Log all sensitive operations
529
+ 3. **Trusting client data** - Always validate server-side
530
+ 4. **Exposing error details** - Sanitize error messages
531
+ 5. **No rate limiting** - Always implement rate limits
532
+
533
+ ## References
534
+
535
+ - Convex Documentation: https://docs.convex.dev/
536
+ - Convex LLMs.txt: https://docs.convex.dev/llms.txt
537
+ - Functions Auth: https://docs.convex.dev/auth/functions-auth
538
+ - Production Security: https://docs.convex.dev/production