@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.
- package/package.json +31 -7
- package/src/firebase/auth/setCustomClaims.ts +26 -4
- package/src/firebase/baseFunction.ts +43 -20
- package/src/firebase/billing/cancelSubscription.ts +9 -1
- package/src/firebase/billing/changePlan.ts +8 -2
- package/src/firebase/billing/createCustomerPortal.ts +16 -2
- package/src/firebase/billing/refreshSubscriptionStatus.ts +3 -1
- package/src/firebase/billing/webhookHandler.ts +13 -1
- package/src/firebase/crud/aggregate.ts +20 -5
- package/src/firebase/crud/create.ts +31 -7
- package/src/firebase/crud/list.ts +7 -22
- package/src/firebase/crud/update.ts +29 -7
- package/src/firebase/oauth/exchangeToken.ts +30 -4
- package/src/firebase/oauth/githubAccess.ts +8 -3
- package/src/firebase/registerCrudFunctions.ts +2 -2
- package/src/firebase/scheduled/checkExpiredSubscriptions.ts +20 -3
- package/src/shared/__tests__/detectFirestore.test.ts +52 -0
- package/src/shared/__tests__/errorHandling.test.ts +144 -0
- package/src/shared/__tests__/idempotency.test.ts +95 -0
- package/src/shared/__tests__/rateLimiter.test.ts +142 -0
- package/src/shared/__tests__/validation.test.ts +172 -0
- package/src/shared/billing/__tests__/createCheckout.test.ts +393 -0
- package/src/shared/billing/__tests__/webhookHandler.test.ts +1091 -0
- package/src/shared/billing/webhookHandler.ts +16 -7
- package/src/shared/errorHandling.ts +16 -54
- package/src/shared/firebase.ts +1 -25
- package/src/shared/oauth/__tests__/exchangeToken.test.ts +116 -0
- package/src/shared/oauth/__tests__/grantAccess.test.ts +237 -0
- package/src/shared/utils/__tests__/functionWrapper.test.ts +122 -0
- package/src/shared/utils/external/subscription.ts +10 -0
- package/src/shared/utils/internal/auth.ts +140 -16
- package/src/shared/utils/internal/rateLimiter.ts +101 -90
- package/src/shared/utils/internal/validation.ts +47 -3
- package/src/shared/utils.ts +154 -39
- package/src/supabase/auth/deleteAccount.ts +59 -0
- package/src/supabase/auth/getCustomClaims.ts +56 -0
- package/src/supabase/auth/getUserAuthStatus.ts +64 -0
- package/src/supabase/auth/removeCustomClaims.ts +75 -0
- package/src/supabase/auth/setCustomClaims.ts +73 -0
- package/src/supabase/baseFunction.ts +302 -0
- package/src/supabase/billing/cancelSubscription.ts +57 -0
- package/src/supabase/billing/changePlan.ts +62 -0
- package/src/supabase/billing/createCheckoutSession.ts +82 -0
- package/src/supabase/billing/createCustomerPortal.ts +58 -0
- package/src/supabase/billing/refreshSubscriptionStatus.ts +89 -0
- package/src/supabase/crud/aggregate.ts +169 -0
- package/src/supabase/crud/create.ts +225 -0
- package/src/supabase/crud/delete.ts +154 -0
- package/src/supabase/crud/get.ts +89 -0
- package/src/supabase/crud/index.ts +24 -0
- package/src/supabase/crud/list.ts +357 -0
- package/src/supabase/crud/update.ts +199 -0
- package/src/supabase/helpers/authProvider.ts +45 -0
- package/src/supabase/index.ts +73 -0
- package/src/supabase/registerCrudFunctions.ts +180 -0
- package/src/supabase/utils/idempotency.ts +141 -0
- package/src/supabase/utils/monitoring.ts +187 -0
- package/src/supabase/utils/rateLimiter.ts +216 -0
- package/src/vercel/api/auth/get-custom-claims.ts +3 -2
- package/src/vercel/api/auth/get-user-auth-status.ts +3 -2
- package/src/vercel/api/auth/remove-custom-claims.ts +3 -2
- package/src/vercel/api/auth/set-custom-claims.ts +5 -2
- package/src/vercel/api/billing/cancel.ts +2 -1
- package/src/vercel/api/billing/change-plan.ts +3 -1
- package/src/vercel/api/billing/customer-portal.ts +4 -1
- package/src/vercel/api/billing/refresh-subscription-status.ts +3 -1
- package/src/vercel/api/billing/webhook-handler.ts +24 -4
- package/src/vercel/api/crud/create.ts +14 -8
- package/src/vercel/api/crud/delete.ts +15 -6
- package/src/vercel/api/crud/get.ts +16 -8
- package/src/vercel/api/crud/list.ts +22 -10
- package/src/vercel/api/crud/update.ts +16 -10
- package/src/vercel/api/oauth/check-github-access.ts +2 -5
- package/src/vercel/api/oauth/grant-github-access.ts +1 -5
- package/src/vercel/api/oauth/revoke-github-access.ts +7 -8
- package/src/vercel/api/utils/cors.ts +13 -2
- 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
|
-
|
|
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
|
-
//
|
|
39
|
-
const uid =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
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
|
-
//
|
|
37
|
-
const uid =
|
|
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 {
|
|
51
|
-
userId
|
|
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
|
-
|
|
57
|
-
|
|
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 {
|
|
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 =
|
|
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
|
-
//
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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);
|