@donotdev/functions 0.0.10 → 0.0.11

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 (77) hide show
  1. package/package.json +31 -7
  2. package/src/firebase/auth/setCustomClaims.ts +26 -4
  3. package/src/firebase/baseFunction.ts +43 -20
  4. package/src/firebase/billing/cancelSubscription.ts +9 -1
  5. package/src/firebase/billing/changePlan.ts +8 -2
  6. package/src/firebase/billing/createCustomerPortal.ts +16 -2
  7. package/src/firebase/billing/refreshSubscriptionStatus.ts +3 -1
  8. package/src/firebase/billing/webhookHandler.ts +13 -1
  9. package/src/firebase/crud/aggregate.ts +20 -5
  10. package/src/firebase/crud/create.ts +31 -7
  11. package/src/firebase/crud/list.ts +7 -22
  12. package/src/firebase/crud/update.ts +29 -7
  13. package/src/firebase/oauth/exchangeToken.ts +30 -4
  14. package/src/firebase/oauth/githubAccess.ts +8 -3
  15. package/src/firebase/registerCrudFunctions.ts +2 -2
  16. package/src/firebase/scheduled/checkExpiredSubscriptions.ts +20 -3
  17. package/src/shared/__tests__/detectFirestore.test.ts +52 -0
  18. package/src/shared/__tests__/errorHandling.test.ts +144 -0
  19. package/src/shared/__tests__/idempotency.test.ts +95 -0
  20. package/src/shared/__tests__/rateLimiter.test.ts +142 -0
  21. package/src/shared/__tests__/validation.test.ts +172 -0
  22. package/src/shared/billing/__tests__/createCheckout.test.ts +393 -0
  23. package/src/shared/billing/__tests__/webhookHandler.test.ts +1091 -0
  24. package/src/shared/billing/webhookHandler.ts +16 -7
  25. package/src/shared/errorHandling.ts +16 -54
  26. package/src/shared/firebase.ts +1 -25
  27. package/src/shared/oauth/__tests__/exchangeToken.test.ts +116 -0
  28. package/src/shared/oauth/__tests__/grantAccess.test.ts +237 -0
  29. package/src/shared/utils/__tests__/functionWrapper.test.ts +122 -0
  30. package/src/shared/utils/external/subscription.ts +10 -0
  31. package/src/shared/utils/internal/auth.ts +140 -16
  32. package/src/shared/utils/internal/rateLimiter.ts +101 -90
  33. package/src/shared/utils/internal/validation.ts +47 -3
  34. package/src/shared/utils.ts +154 -39
  35. package/src/supabase/auth/deleteAccount.ts +59 -0
  36. package/src/supabase/auth/getCustomClaims.ts +56 -0
  37. package/src/supabase/auth/getUserAuthStatus.ts +64 -0
  38. package/src/supabase/auth/removeCustomClaims.ts +75 -0
  39. package/src/supabase/auth/setCustomClaims.ts +73 -0
  40. package/src/supabase/baseFunction.ts +302 -0
  41. package/src/supabase/billing/cancelSubscription.ts +57 -0
  42. package/src/supabase/billing/changePlan.ts +62 -0
  43. package/src/supabase/billing/createCheckoutSession.ts +82 -0
  44. package/src/supabase/billing/createCustomerPortal.ts +58 -0
  45. package/src/supabase/billing/refreshSubscriptionStatus.ts +89 -0
  46. package/src/supabase/crud/aggregate.ts +169 -0
  47. package/src/supabase/crud/create.ts +225 -0
  48. package/src/supabase/crud/delete.ts +154 -0
  49. package/src/supabase/crud/get.ts +89 -0
  50. package/src/supabase/crud/index.ts +24 -0
  51. package/src/supabase/crud/list.ts +357 -0
  52. package/src/supabase/crud/update.ts +199 -0
  53. package/src/supabase/helpers/authProvider.ts +45 -0
  54. package/src/supabase/index.ts +73 -0
  55. package/src/supabase/registerCrudFunctions.ts +180 -0
  56. package/src/supabase/utils/idempotency.ts +141 -0
  57. package/src/supabase/utils/monitoring.ts +187 -0
  58. package/src/supabase/utils/rateLimiter.ts +216 -0
  59. package/src/vercel/api/auth/get-custom-claims.ts +3 -2
  60. package/src/vercel/api/auth/get-user-auth-status.ts +3 -2
  61. package/src/vercel/api/auth/remove-custom-claims.ts +3 -2
  62. package/src/vercel/api/auth/set-custom-claims.ts +5 -2
  63. package/src/vercel/api/billing/cancel.ts +2 -1
  64. package/src/vercel/api/billing/change-plan.ts +3 -1
  65. package/src/vercel/api/billing/customer-portal.ts +4 -1
  66. package/src/vercel/api/billing/refresh-subscription-status.ts +3 -1
  67. package/src/vercel/api/billing/webhook-handler.ts +24 -4
  68. package/src/vercel/api/crud/create.ts +14 -8
  69. package/src/vercel/api/crud/delete.ts +15 -6
  70. package/src/vercel/api/crud/get.ts +16 -8
  71. package/src/vercel/api/crud/list.ts +22 -10
  72. package/src/vercel/api/crud/update.ts +16 -10
  73. package/src/vercel/api/oauth/check-github-access.ts +2 -5
  74. package/src/vercel/api/oauth/grant-github-access.ts +1 -5
  75. package/src/vercel/api/oauth/revoke-github-access.ts +7 -8
  76. package/src/vercel/api/utils/cors.ts +13 -2
  77. package/src/vercel/baseFunction.ts +40 -25
@@ -19,10 +19,8 @@ import {
19
19
  transformFirestoreData,
20
20
  } from '../../../shared/index.js';
21
21
  import { updateMetadata } from '../../../shared/index.js';
22
- import {
23
- validateDocument,
24
- assertAuthenticated,
25
- } from '../../../shared/utils.js';
22
+ import { validateCollectionName, validateDocument } from '../../../shared/utils.js';
23
+ import { verifyAuthToken } from '../../../shared/utils/internal/auth.js';
26
24
 
27
25
  import type { NextApiRequest, NextApiResponse } from 'next';
28
26
 
@@ -35,15 +33,18 @@ export default async function handler(
35
33
  }
36
34
 
37
35
  try {
38
- // Verify authentication
39
- const uid = assertAuthenticated(req.headers.authorization);
36
+ // C1: verify JWT — previously only checked header presence.
37
+ const uid = await verifyAuthToken(req);
40
38
 
41
39
  const { schema, id, payload } = req.body as UpdateEntityData<any>;
42
40
 
43
41
  if (!schema || !id || !payload) {
44
- throw handleError(new Error('Missing schema, id, or payload'));
42
+ handleError(new Error('Missing schema, id, or payload'));
45
43
  }
46
44
 
45
+ // W22: Validate collection name from client-supplied schema
46
+ validateCollectionName(schema.metadata.collection);
47
+
47
48
  const db = getFirebaseAdminFirestore();
48
49
 
49
50
  // Get current document to merge with payload for status check
@@ -53,7 +54,7 @@ export default async function handler(
53
54
  .get();
54
55
 
55
56
  if (!currentDoc.exists) {
56
- throw handleError(new Error('Document not found'));
57
+ handleError(new Error('Document not found'));
57
58
  }
58
59
 
59
60
  // Merge current data with payload to determine resulting status
@@ -84,7 +85,7 @@ export default async function handler(
84
85
  const doc = await db.collection(schema.metadata.collection).doc(id).get();
85
86
 
86
87
  if (!doc.exists) {
87
- throw handleError(new Error('Document not found'));
88
+ handleError(new Error('Document not found'));
88
89
  }
89
90
 
90
91
  // Transform the document data back to the application format
@@ -95,6 +96,11 @@ export default async function handler(
95
96
 
96
97
  return res.status(200).json(result);
97
98
  } catch (error) {
98
- throw handleError(error);
99
+ try {
100
+ handleError(error);
101
+ } catch (handledError: any) {
102
+ const status = handledError.code === 'invalid-argument' ? 400 : 500;
103
+ return res.status(status).json({ error: handledError.message, code: handledError.code });
104
+ }
99
105
  }
100
106
  }
@@ -36,11 +36,8 @@ async function checkGitHubAccessLogic(
36
36
  data: CheckGitHubAccessRequest,
37
37
  context: { uid: string }
38
38
  ) {
39
- const { userId, githubUsername, repoConfig } = data;
40
-
41
- if (!userId) {
42
- throw new Error('User ID is required');
43
- }
39
+ const { githubUsername, repoConfig } = data;
40
+ const userId = context.uid;
44
41
 
45
42
  if (!githubUsername) {
46
43
  throw new Error('GitHub username is required');
@@ -37,16 +37,12 @@ async function grantGitHubAccessLogic(
37
37
  context: { uid: string }
38
38
  ) {
39
39
  const {
40
- userId,
41
40
  githubUsername,
42
41
  repoConfig,
43
42
  permission = 'push',
44
43
  customClaims,
45
44
  } = data;
46
-
47
- if (!userId) {
48
- throw new Error('User ID is required');
49
- }
45
+ const userId = context.uid;
50
46
 
51
47
  if (!githubUsername) {
52
48
  throw new Error('GitHub username is required');
@@ -19,7 +19,7 @@ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
19
19
 
20
20
  import { handleError } from '../../../shared/errorHandling.js';
21
21
  import { GitHubApiService } from '../../../shared/index.js';
22
- import { assertAuthenticated } from '../../../shared/utils.js';
22
+ import { verifyAuthToken } from '../../../shared/utils/internal/auth.js';
23
23
 
24
24
  import type { NextApiRequest, NextApiResponse } from 'next';
25
25
 
@@ -33,8 +33,8 @@ export default async function handler(
33
33
  }
34
34
 
35
35
  try {
36
- // Verify authentication
37
- const uid = assertAuthenticated(req.headers.authorization);
36
+ // C1: verify JWT — previously only checked header presence.
37
+ const uid = await verifyAuthToken(req);
38
38
 
39
39
  // Use provided schema or default to framework schema
40
40
  const schema = customSchema || revokeGitHubAccessSchema;
@@ -47,15 +47,14 @@ export default async function handler(
47
47
  });
48
48
  }
49
49
 
50
- const { userId, githubUsername, repoConfig } = validationResult.output as {
51
- userId: string;
50
+ const { githubUsername, repoConfig } = validationResult.output as {
51
+ userId?: string;
52
52
  githubUsername: string;
53
53
  repoConfig: { owner: string; repo: string };
54
54
  };
55
55
 
56
- if (!userId) {
57
- throw handleError(new Error('User ID is required'));
58
- }
56
+ // C1/IDOR: Always use verified uid from token — ignore client-supplied userId.
57
+ const userId = uid;
59
58
 
60
59
  if (!githubUsername) {
61
60
  throw handleError(new Error('GitHub username is required'));
@@ -12,18 +12,29 @@
12
12
  import type { NextApiRequest, NextApiResponse } from 'next';
13
13
 
14
14
  /**
15
- * CORS configuration for Vercel functions
15
+ * CORS configuration for Vercel functions.
16
+ *
17
+ * **Architecture decision — CORS wildcard (`*`) as framework default:**
18
+ * The wildcard origin is an intentional development-convenience default, not a
19
+ * security oversight. Consumer apps MUST override `allowedOrigins` in their
20
+ * deployment configuration for production environments. The framework provides
21
+ * `configureCors({ allowedOrigins: ['https://myapp.example'] })` for this purpose.
22
+ *
23
+ * C2: Allow-Credentials:true is incompatible with wildcard origin per the Fetch spec.
24
+ * Credentialed requests require an explicit origin. Removed Allow-Credentials header so
25
+ * the wildcard origin remains valid. Consumers that need credentialed cross-origin requests
26
+ * must replace '*' with their specific origin and re-add Allow-Credentials:true.
16
27
  *
17
28
  * @version 0.0.1
18
29
  * @since 0.0.1
19
30
  * @author AMBROISE PARK Consulting
20
31
  */
21
32
  export const corsHeaders = {
33
+ // Framework default — consumers override via configureCors({ allowedOrigins }) in production
22
34
  'Access-Control-Allow-Origin': '*',
23
35
  'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
24
36
  'Access-Control-Allow-Headers':
25
37
  'Content-Type, Authorization, X-Requested-With',
26
- 'Access-Control-Allow-Credentials': 'true',
27
38
  'Access-Control-Max-Age': '86400',
28
39
  };
29
40
 
@@ -17,8 +17,10 @@ import {
17
17
  checkRateLimitWithFirestore,
18
18
  DEFAULT_RATE_LIMITS,
19
19
  } from '../shared/utils/internal/rateLimiter.js';
20
- import { assertAuthenticated } from '../shared/utils.js';
20
+ import { verifyAuthToken } from '../shared/utils/internal/auth.js';
21
+ import type { SecurityContext } from '@donotdev/core';
21
22
 
23
+ import type { AuthProvider } from '../shared/utils/internal/auth.js';
22
24
  import type { NextApiRequest, NextApiResponse } from 'next';
23
25
 
24
26
  /**
@@ -40,7 +42,10 @@ export function createVercelBaseFunction<TRequest, TResponse>(
40
42
  context: {
41
43
  uid: string;
42
44
  }
43
- ) => Promise<TResponse>
45
+ ) => Promise<TResponse>,
46
+ security?: SecurityContext,
47
+ /** Auth provider for token verification. Auto-detects via provider registry if omitted. */
48
+ provider?: AuthProvider
44
49
  ) {
45
50
  return async (
46
51
  req: NextApiRequest,
@@ -64,8 +69,8 @@ export function createVercelBaseFunction<TRequest, TResponse>(
64
69
 
65
70
  const validatedData = validationResult.output;
66
71
 
67
- // Verify authentication
68
- const uid = assertAuthenticated(req.headers.authorization);
72
+ // Verify authentication — extracts Bearer token and verifies via configured provider
73
+ const uid = await verifyAuthToken(req, provider);
69
74
 
70
75
  // Rate limiting
71
76
  const rateLimitKey = `${operation}_${uid}`;
@@ -77,6 +82,11 @@ export function createVercelBaseFunction<TRequest, TResponse>(
77
82
  );
78
83
 
79
84
  if (!rateLimitResult.allowed) {
85
+ security?.audit({
86
+ type: 'rate_limit.exceeded',
87
+ userId: uid,
88
+ metadata: { operation, remaining: 0 },
89
+ });
80
90
  return res.status(429).json({
81
91
  error: `Rate limit exceeded. Try again in ${rateLimitResult.blockRemainingSeconds} seconds.`,
82
92
  retryAfter: rateLimitResult.blockRemainingSeconds,
@@ -86,30 +96,35 @@ export function createVercelBaseFunction<TRequest, TResponse>(
86
96
  // Call user's business logic
87
97
  const result = await businessLogic(req, res, validatedData, { uid });
88
98
 
89
- // Record metrics
90
- await recordPaymentMetrics({
91
- operation,
92
- userId: uid,
93
- status: 'success',
94
- timestamp: new Date().toISOString(),
95
- metadata: {
96
- requestId: (req.headers['x-request-id'] as string) || 'unknown',
97
- },
98
- });
99
+ // W3: Only record metrics when explicitly enabled — avoids unconditional
100
+ // Firestore writes on every request.
101
+ if (process.env.ENABLE_METRICS === 'true') {
102
+ await recordPaymentMetrics({
103
+ operation,
104
+ userId: uid,
105
+ status: 'success',
106
+ timestamp: new Date().toISOString(),
107
+ metadata: {
108
+ requestId: (req.headers['x-request-id'] as string) || 'unknown',
109
+ },
110
+ });
111
+ }
99
112
 
100
113
  return result;
101
114
  } catch (error) {
102
- // Record error metrics
103
- await recordPaymentMetrics({
104
- operation,
105
- userId: req.headers.authorization ? 'authenticated' : 'anonymous',
106
- status: 'failed' as const,
107
- timestamp: new Date().toISOString(),
108
- metadata: {
109
- error: error instanceof Error ? error.message : 'Unknown error',
110
- requestId: (req.headers['x-request-id'] as string) || 'unknown',
111
- },
112
- });
115
+ // W3: Only record error metrics when enabled.
116
+ if (process.env.ENABLE_METRICS === 'true') {
117
+ await recordPaymentMetrics({
118
+ operation,
119
+ userId: req.headers.authorization ? 'authenticated' : 'anonymous',
120
+ status: 'failed' as const,
121
+ timestamp: new Date().toISOString(),
122
+ metadata: {
123
+ error: error instanceof Error ? error.message : 'Unknown error',
124
+ requestId: (req.headers['x-request-id'] as string) || 'unknown',
125
+ },
126
+ });
127
+ }
113
128
 
114
129
  // Handle error and return appropriate response
115
130
  const errorResponse = handleVercelError(error);