@donotdev/functions 0.0.6 → 0.0.8

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 (56) hide show
  1. package/lib/firebase/index.js +8026 -32339
  2. package/lib/firebase/index.js.map +4 -4
  3. package/lib/shared/index.js +5973 -31507
  4. package/lib/shared/index.js.map +4 -4
  5. package/lib/vercel/api/index.js +4173 -30252
  6. package/lib/vercel/api/index.js.map +4 -4
  7. package/package.json +3 -3
  8. package/src/firebase/auth/getCustomClaims.ts +2 -1
  9. package/src/firebase/auth/getUserAuthStatus.ts +2 -1
  10. package/src/firebase/auth/removeCustomClaims.ts +2 -1
  11. package/src/firebase/auth/setCustomClaims.ts +3 -1
  12. package/src/firebase/baseFunction.ts +167 -65
  13. package/src/firebase/billing/cancelSubscription.ts +2 -1
  14. package/src/firebase/billing/changePlan.ts +1 -1
  15. package/src/firebase/billing/createCheckoutSession.ts +2 -2
  16. package/src/firebase/billing/createCustomerPortal.ts +2 -1
  17. package/src/firebase/billing/refreshSubscriptionStatus.ts +1 -1
  18. package/src/firebase/billing/webhookHandler.ts +3 -1
  19. package/src/firebase/config/constants.ts +12 -2
  20. package/src/firebase/crud/aggregate.ts +2 -2
  21. package/src/firebase/crud/create.ts +10 -12
  22. package/src/firebase/crud/delete.ts +9 -11
  23. package/src/firebase/crud/get.ts +21 -11
  24. package/src/firebase/crud/list.ts +86 -29
  25. package/src/firebase/crud/update.ts +9 -11
  26. package/src/firebase/helpers/githubAccessHelper.ts +2 -1
  27. package/src/firebase/helpers/githubTeamAccessHelper.ts +2 -1
  28. package/src/firebase/index.ts +7 -0
  29. package/src/firebase/oauth/disconnect.ts +1 -1
  30. package/src/firebase/oauth/exchangeToken.ts +4 -4
  31. package/src/firebase/oauth/getConnections.ts +1 -1
  32. package/src/firebase/oauth/githubAccess.ts +3 -2
  33. package/src/firebase/oauth/refreshToken.ts +4 -4
  34. package/src/firebase/registerCrudFunctions.ts +127 -0
  35. package/src/firebase/scheduled/checkExpiredSubscriptions.ts +3 -2
  36. package/src/shared/billing/webhookHandler.ts +1 -1
  37. package/src/shared/errorHandling.ts +1 -1
  38. package/src/shared/utils/external/subscription.ts +19 -6
  39. package/src/shared/utils/firebaseHelpers.ts +3 -1
  40. package/src/shared/utils/internal/idempotency.ts +2 -1
  41. package/src/shared/utils/internal/monitoring.ts +2 -1
  42. package/src/shared/utils/internal/rateLimiter.ts +2 -1
  43. package/src/shared/utils.ts +56 -7
  44. package/src/vercel/api/billing/cancel.ts +2 -1
  45. package/src/vercel/api/billing/change-plan.ts +1 -1
  46. package/src/vercel/api/billing/create-checkout-session.ts +2 -2
  47. package/src/vercel/api/billing/customer-portal.ts +2 -1
  48. package/src/vercel/api/billing/refresh-subscription-status.ts +1 -1
  49. package/src/vercel/api/crud/create.ts +2 -3
  50. package/src/vercel/api/crud/delete.ts +0 -1
  51. package/src/vercel/api/crud/get.ts +2 -3
  52. package/src/vercel/api/crud/list.ts +2 -3
  53. package/src/vercel/api/crud/update.ts +2 -3
  54. package/src/vercel/api/oauth/check-github-access.ts +1 -1
  55. package/src/vercel/api/oauth/grant-github-access.ts +1 -1
  56. package/src/vercel/api/oauth/revoke-github-access.ts +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donotdev/functions",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "private": false,
5
5
  "description": "Backend functions for DoNotDev Framework - Firebase, Vercel, and platform-agnostic implementations for auth, billing, CRUD, and OAuth",
6
6
  "main": "./lib/firebase/index.js",
@@ -42,8 +42,8 @@
42
42
  "serve": "firebase emulators:start --only functions"
43
43
  },
44
44
  "dependencies": {
45
- "@donotdev/core": "^0.0.13",
46
- "@donotdev/firebase": "^0.0.5"
45
+ "@donotdev/core": "^0.0.18",
46
+ "@donotdev/firebase": "^0.0.7"
47
47
  },
48
48
  "peerDependencies": {
49
49
  "@sentry/node": "^10.33.0",
@@ -9,9 +9,10 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
13
12
  import * as v from 'valibot';
14
13
 
14
+ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
15
+
15
16
  import { createBaseFunction } from '../baseFunction.js';
16
17
  import { AUTH_CONFIG } from '../config/constants.js';
17
18
 
@@ -9,9 +9,10 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
13
12
  import * as v from 'valibot';
14
13
 
14
+ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
15
+
15
16
  import { createBaseFunction } from '../baseFunction.js';
16
17
  import { AUTH_CONFIG } from '../config/constants.js';
17
18
 
@@ -9,9 +9,10 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
13
12
  import * as v from 'valibot';
14
13
 
14
+ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
15
+
15
16
  import { createBaseFunction } from '../baseFunction.js';
16
17
  import { AUTH_CONFIG } from '../config/constants.js';
17
18
 
@@ -9,11 +9,13 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
+ import * as v from 'valibot';
13
+
12
14
  import {
13
15
  getFirebaseAdminAuth,
14
16
  getFirebaseAdminFirestore,
15
17
  } from '@donotdev/firebase/server';
16
- import * as v from 'valibot';
18
+
17
19
  import { createBaseFunction } from '../baseFunction.js';
18
20
  import { AUTH_CONFIG } from '../config/constants.js';
19
21
 
@@ -4,28 +4,90 @@
4
4
  * @fileoverview Base Firebase function that handles all common concerns
5
5
  * @description Rate limiting, monitoring, authentication, validation - all in one place
6
6
  *
7
- * @version 0.0.1
7
+ * @version 0.0.2
8
8
  * @since 0.0.1
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
12
  import { logger } from 'firebase-functions/v2';
13
- import { onCall, type CallableRequest } from 'firebase-functions/v2/https';
13
+ import { onCall } from 'firebase-functions/v2/https';
14
14
  import * as v from 'valibot';
15
15
 
16
+ import { hasRoleAccess } from '@donotdev/core/server';
17
+ import type { UserRole } from '@donotdev/core/server';
18
+
16
19
  import { handleError } from '../shared/errorHandling.js';
17
- import { recordPaymentMetrics } from '../shared/utils/internal/monitoring.js';
18
- import {
19
- checkRateLimitWithFirestore,
20
- DEFAULT_RATE_LIMITS,
21
- } from '../shared/utils/internal/rateLimiter.js';
22
- import { assertAuthenticated } from '../shared/utils.js';
20
+ import { assertAuthenticated, getUserRole } from '../shared/utils.js';
21
+
22
+ import type { CallableRequest } from 'firebase-functions/v2/https';
23
+
24
+ // Optional monitoring imports - only used when enabled
25
+ // Lazy loaded to avoid unnecessary Firestore operations
26
+ let checkRateLimitWithFirestore:
27
+ | typeof import('../shared/utils/internal/rateLimiter.js').checkRateLimitWithFirestore
28
+ | null = null;
29
+ let DEFAULT_RATE_LIMITS:
30
+ | typeof import('../shared/utils/internal/rateLimiter.js').DEFAULT_RATE_LIMITS
31
+ | null = null;
32
+ let recordPaymentMetrics:
33
+ | typeof import('../shared/utils/internal/monitoring.js').recordPaymentMetrics
34
+ | null = null;
35
+
36
+ async function loadRateLimiter() {
37
+ if (!checkRateLimitWithFirestore) {
38
+ const mod = await import('../shared/utils/internal/rateLimiter.js');
39
+ checkRateLimitWithFirestore = mod.checkRateLimitWithFirestore;
40
+ DEFAULT_RATE_LIMITS = mod.DEFAULT_RATE_LIMITS;
41
+ }
42
+ return { checkRateLimitWithFirestore, DEFAULT_RATE_LIMITS };
43
+ }
44
+
45
+ async function loadMonitoring() {
46
+ if (!recordPaymentMetrics) {
47
+ const mod = await import('../shared/utils/internal/monitoring.js');
48
+ recordPaymentMetrics = mod.recordPaymentMetrics;
49
+ }
50
+ return recordPaymentMetrics;
51
+ }
52
+
53
+ /**
54
+ * Extract client IP from Firebase callable request
55
+ * Handles proxied requests (X-Forwarded-For header)
56
+ */
57
+ function getClientIp(request: CallableRequest<unknown>): string {
58
+ // Try X-Forwarded-For first (common for proxied requests)
59
+ const forwardedFor = request.rawRequest.headers['x-forwarded-for'];
60
+ if (forwardedFor) {
61
+ // Take the first IP (client IP) from the comma-separated list
62
+ let ips: string | string[] | undefined;
63
+ if (Array.isArray(forwardedFor)) {
64
+ ips = forwardedFor.length > 0 ? forwardedFor[0] : undefined;
65
+ } else {
66
+ ips = forwardedFor;
67
+ }
68
+ if (ips && typeof ips === 'string') {
69
+ const firstIp = ips.split(',')[0];
70
+ return firstIp ? firstIp.trim() : 'unknown';
71
+ }
72
+ }
73
+
74
+ // Fallback to raw IP
75
+ const rawIp =
76
+ request.rawRequest.ip || request.rawRequest.socket?.remoteAddress;
77
+ return rawIp || 'unknown';
78
+ }
23
79
 
24
80
  /**
25
81
  * Base Firebase function that handles all common concerns
26
82
  * Users just provide their business logic
27
83
  *
28
- * @version 0.0.1
84
+ * @param config - Firebase function config (region, memory, etc.)
85
+ * @param schema - Valibot schema for request validation
86
+ * @param operation - Operation name for logging/metrics
87
+ * @param businessLogic - The actual business logic to execute
88
+ * @param requiredRole - Minimum role required (default: 'user' for backwards compatibility)
89
+ *
90
+ * @version 0.0.2
29
91
  * @since 0.0.1
30
92
  * @author AMBROISE PARK Consulting
31
93
  */
@@ -37,9 +99,11 @@ export function createBaseFunction<TRequest, TResponse>(
37
99
  data: TRequest,
38
100
  context: {
39
101
  uid: string;
102
+ userRole: UserRole;
40
103
  request: CallableRequest<TRequest>;
41
104
  }
42
- ) => Promise<TResponse>
105
+ ) => Promise<TResponse>,
106
+ requiredRole: UserRole = 'user'
43
107
  ) {
44
108
  // Validate schema at function creation time (framework-level robustness)
45
109
  if (!schema) {
@@ -110,21 +174,14 @@ export function createBaseFunction<TRequest, TResponse>(
110
174
  `Schema is undefined for ${operation} - this indicates a bundling/import issue`
111
175
  );
112
176
  }
113
- if (request.data === undefined || request.data === null) {
114
- logger.error(`Request data is missing for ${operation}`, {
115
- operation,
116
- dataValue: request.data,
117
- hasAuth: !!request.auth,
118
- userId: request.auth?.uid,
119
- });
120
- throw new Error(
121
- `Request data is required for ${operation} but was ${request.data === null ? 'null' : 'undefined'}`
122
- );
123
- }
177
+
178
+ // Normalize undefined/null to empty object for validation
179
+ // This allows callable functions with no parameters to work correctly
180
+ const requestData = request.data ?? {};
124
181
 
125
182
  let validatedData: TRequest;
126
183
  try {
127
- validatedData = v.parse(schema, request.data);
184
+ validatedData = v.parse(schema, requestData);
128
185
  } catch (parseError) {
129
186
  logger.error(`Schema validation failed for ${operation}`, {
130
187
  error:
@@ -136,57 +193,102 @@ export function createBaseFunction<TRequest, TResponse>(
136
193
  throw parseError;
137
194
  }
138
195
 
139
- // Verify authentication
140
- const uid = assertAuthenticated(request.auth);
141
-
142
- // Rate limiting
143
- const rateLimitKey = `${operation}_${uid}`;
144
- const rateLimitConfig =
145
- (DEFAULT_RATE_LIMITS as any)[operation] ||
146
- DEFAULT_RATE_LIMITS.checkout;
147
- const rateLimitResult = await checkRateLimitWithFirestore(
148
- rateLimitKey,
149
- rateLimitConfig
150
- );
151
-
152
- if (!rateLimitResult.allowed) {
153
- logger.warn(`Rate limit exceeded for ${operation}`, {
154
- userId: uid,
155
- remaining: rateLimitResult.remaining,
156
- resetAt: rateLimitResult.resetAt,
157
- });
158
- throw new Error(
159
- `Rate limit exceeded. Try again in ${rateLimitResult.blockRemainingSeconds} seconds.`
196
+ // Get user role from auth context
197
+ const userRole = getUserRole(request.auth);
198
+ let uid: string;
199
+
200
+ // Role-based access control
201
+ if (requiredRole === 'guest') {
202
+ // Guest access: no authentication required
203
+ // Use 'guest' as UID for unauthenticated users
204
+ uid = request.auth?.uid || 'guest';
205
+ } else {
206
+ // Non-guest access: require authentication
207
+ uid = assertAuthenticated(request.auth);
208
+
209
+ // Verify user has required role level
210
+ if (!hasRoleAccess(userRole, requiredRole)) {
211
+ logger.warn(`Insufficient role for ${operation}`, {
212
+ userId: uid,
213
+ userRole,
214
+ requiredRole,
215
+ });
216
+ throw new Error(
217
+ `Access denied. Required role: ${requiredRole}, your role: ${userRole}`
218
+ );
219
+ }
220
+ }
221
+
222
+ // Rate limiting (only if enabled via ENABLE_RATE_LIMITING env var)
223
+ if (process.env.ENABLE_RATE_LIMITING === 'true') {
224
+ const {
225
+ checkRateLimitWithFirestore: checkLimit,
226
+ DEFAULT_RATE_LIMITS: limits,
227
+ } = await loadRateLimiter();
228
+
229
+ // Use IP-based key for guest operations, UID-based for authenticated
230
+ const rateLimitIdentifier =
231
+ requiredRole === 'guest' && uid === 'guest'
232
+ ? `ip_${getClientIp(request)}`
233
+ : `uid_${uid}`;
234
+ const rateLimitKey = `${operation}_${rateLimitIdentifier}`;
235
+ const rateLimitConfig = (limits as any)[operation] || limits!.api;
236
+ const rateLimitResult = await checkLimit!(
237
+ rateLimitKey,
238
+ rateLimitConfig
160
239
  );
240
+
241
+ if (!rateLimitResult.allowed) {
242
+ logger.warn(`Rate limit exceeded for ${operation}`, {
243
+ identifier: rateLimitIdentifier,
244
+ remaining: rateLimitResult.remaining,
245
+ resetAt: rateLimitResult.resetAt,
246
+ });
247
+ throw new Error(
248
+ `Rate limit exceeded. Try again in ${rateLimitResult.blockRemainingSeconds} seconds.`
249
+ );
250
+ }
161
251
  }
162
252
 
163
253
  // Call user's business logic
164
- const result = await businessLogic(validatedData, { uid, request });
165
-
166
- // Record metrics
167
- await recordPaymentMetrics({
168
- operation,
169
- userId: uid,
170
- status: 'success',
171
- timestamp: new Date().toISOString(),
172
- metadata: {
173
- requestId: request.rawRequest.headers['x-request-id'] || 'unknown',
174
- },
254
+ const result = await businessLogic(validatedData, {
255
+ uid,
256
+ userRole,
257
+ request,
175
258
  });
176
259
 
260
+ // Record metrics (only if enabled via ENABLE_METRICS env var)
261
+ if (process.env.ENABLE_METRICS === 'true') {
262
+ const recordMetrics = await loadMonitoring();
263
+ await recordMetrics!({
264
+ operation,
265
+ userId: uid,
266
+ status: 'success',
267
+ timestamp: new Date().toISOString(),
268
+ metadata: {
269
+ requestId:
270
+ request.rawRequest.headers['x-request-id'] || 'unknown',
271
+ },
272
+ });
273
+ }
274
+
177
275
  return result;
178
276
  } catch (error) {
179
- // Record error metrics
180
- await recordPaymentMetrics({
181
- operation,
182
- userId: request.auth?.uid || 'anonymous',
183
- status: 'failed' as const,
184
- timestamp: new Date().toISOString(),
185
- metadata: {
186
- error: error instanceof Error ? error.message : 'Unknown error',
187
- requestId: request.rawRequest.headers['x-request-id'] || 'unknown',
188
- },
189
- });
277
+ // Record error metrics (only if enabled)
278
+ if (process.env.ENABLE_METRICS === 'true') {
279
+ const recordMetrics = await loadMonitoring();
280
+ await recordMetrics!({
281
+ operation,
282
+ userId: request.auth?.uid || 'anonymous',
283
+ status: 'failed' as const,
284
+ timestamp: new Date().toISOString(),
285
+ metadata: {
286
+ error: error instanceof Error ? error.message : 'Unknown error',
287
+ requestId:
288
+ request.rawRequest.headers['x-request-id'] || 'unknown',
289
+ },
290
+ });
291
+ }
190
292
 
191
293
  throw handleError(error);
192
294
  }
@@ -9,9 +9,10 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
13
12
  import * as v from 'valibot';
14
13
 
14
+ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
15
+
15
16
  import { cancelUserSubscription } from '../../shared/billing/helpers/subscriptionManagement.js';
16
17
  import { createBaseFunction } from '../baseFunction.js';
17
18
  import { STRIPE_CONFIG } from '../config/constants.js';
@@ -9,11 +9,11 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
13
12
  import Stripe from 'stripe';
14
13
  import * as v from 'valibot';
15
14
 
16
15
  import type { StripeBackConfig } from '@donotdev/core/server';
16
+ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
17
17
 
18
18
  import { updateUserSubscription } from '../../shared/billing/helpers/updateUserSubscription.js';
19
19
  import { handleError } from '../../shared/errorHandling.js';
@@ -9,7 +9,6 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
13
12
  import { logger } from 'firebase-functions/v2';
14
13
 
15
14
  import type {
@@ -18,6 +17,7 @@ import type {
18
17
  } from '@donotdev/core/server';
19
18
  import { STRIPE_MODES } from '@donotdev/core/server';
20
19
  import { CreateCheckoutSessionRequestSchema } from '@donotdev/core/server';
20
+ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
21
21
 
22
22
  import { handleError } from '../../shared/errorHandling.js';
23
23
  import {
@@ -25,10 +25,10 @@ import {
25
25
  validateMetadata,
26
26
  } from '../../shared/utils/validation.js';
27
27
  import { stripe, validateStripeEnvironment } from '../../shared/utils.js';
28
+ import { initStripe } from '../../shared/utils.js'; // ✅ IMPORT INIT
28
29
  import { createBaseFunction } from '../baseFunction.js';
29
30
  import { STRIPE_CONFIG } from '../config/constants.js';
30
31
  import { stripeSecretKey } from '../config/secrets.js'; // ✅ IMPORT SECRET
31
- import { initStripe } from '../../shared/utils.js'; // ✅ IMPORT INIT
32
32
 
33
33
  import type {
34
34
  CallableFunction,
@@ -9,9 +9,10 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
13
12
  import * as v from 'valibot';
14
13
 
14
+ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
15
+
15
16
  import { handleError } from '../../shared/errorHandling.js';
16
17
  import {
17
18
  stripe,
@@ -9,11 +9,11 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
13
12
  import Stripe from 'stripe';
14
13
  import * as v from 'valibot';
15
14
 
16
15
  import type { RefreshSubscriptionRequest } from '@donotdev/core/server';
16
+ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
17
17
 
18
18
  import { stripe, validateStripeEnvironment } from '../../shared/utils.js';
19
19
  import { createBaseFunction } from '../baseFunction.js';
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { logger } from 'firebase-functions/v2';
13
- import { onRequest, type HttpsFunction } from 'firebase-functions/v2/https';
13
+ import { onRequest } from 'firebase-functions/v2/https';
14
14
 
15
15
  import type { StripeBackConfig } from '@donotdev/core/server';
16
16
 
@@ -25,6 +25,8 @@ import {
25
25
  import { STRIPE_CONFIG } from '../config/constants.js';
26
26
  import { stripeSecretKey, stripeWebhookSecret } from '../config/secrets.js'; // ✅ IMPORT SECRETS
27
27
 
28
+ import type { HttpsFunction } from 'firebase-functions/v2/https';
29
+
28
30
  /**
29
31
  * Read raw body from request stream
30
32
  */
@@ -18,13 +18,13 @@ const ENFORCE_APP_CHECK = process.env.ENFORCE_APP_CHECK === 'true';
18
18
  /** Base config inherited by all function configs */
19
19
  const BASE_CONFIG = {
20
20
  region: FIREBASE_REGION,
21
- invoker: 'public', // Cloud Run allows HTTP, onCall validates Firebase Auth
22
21
  ...(ENFORCE_APP_CHECK && { enforceAppCheck: true }),
23
22
  } as const;
24
23
 
25
24
  /** Default function config */
26
25
  export const FUNCTION_CONFIG = {
27
26
  ...BASE_CONFIG,
27
+ invoker: 'public', // Cloud Run allows HTTP, onCall validates Firebase Auth
28
28
  memory: '1GiB' as const,
29
29
  timeoutSeconds: 60,
30
30
  cors: true, // Enable CORS by default for all functions (required for web apps)
@@ -48,9 +48,19 @@ export const AUTH_CONFIG = {
48
48
  timeoutSeconds: 20,
49
49
  } as const;
50
50
 
51
- /** CRUD functions */
51
+ /** CRUD functions - all use public invoker (security enforced via role-based access in code) */
52
52
  export const CRUD_CONFIG = {
53
53
  ...BASE_CONFIG,
54
+ invoker: 'public', // Cloud Run allows HTTP, onCall validates Firebase Auth + role-based access
55
+ memory: '256MiB' as const,
56
+ timeoutSeconds: 15,
57
+ cors: true, // Enable CORS for cross-origin requests (required for web apps)
58
+ } as const;
59
+
60
+ /** CRUD read functions (get, list) - can be public for guest access */
61
+ export const CRUD_READ_CONFIG = {
62
+ ...BASE_CONFIG,
63
+ invoker: 'public', // Cloud Run allows HTTP, onCall validates Firebase Auth
54
64
  memory: '256MiB' as const,
55
65
  timeoutSeconds: 15,
56
66
  cors: true, // Enable CORS for cross-origin requests (required for web apps)
@@ -10,8 +10,6 @@
10
10
  * @author AMBROISE PARK Consulting
11
11
  */
12
12
 
13
- import type { CallableRequest } from 'firebase-functions/v2/https';
14
-
15
13
  import * as v from 'valibot';
16
14
 
17
15
  import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
@@ -21,6 +19,8 @@ import { DoNotDevError } from '../../shared/utils.js';
21
19
  import { createBaseFunction } from '../baseFunction.js';
22
20
  import { CRUD_CONFIG } from '../config/constants.js';
23
21
 
22
+ import type { CallableRequest } from 'firebase-functions/v2/https';
23
+
24
24
  /** Supported aggregation operations */
25
25
  export type AggregateOperation = 'count' | 'sum' | 'avg' | 'min' | 'max';
26
26
 
@@ -4,22 +4,23 @@
4
4
  * @fileoverview Generic function to create an entity.
5
5
  * @description Provides a reusable implementation for creating documents in Firestore with validation.
6
6
  *
7
- * @version 0.0.1
7
+ * @version 0.0.2
8
8
  * @since 0.0.1
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
12
  import * as v from 'valibot';
13
13
 
14
- import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
15
14
  import { DEFAULT_STATUS_VALUE } from '@donotdev/core/server';
15
+ import type { UserRole } from '@donotdev/core/server';
16
+ import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
16
17
 
17
18
  import {
18
19
  prepareForFirestore,
19
20
  transformFirestoreData,
20
21
  } from '../../shared/index.js';
21
22
  import { createMetadata } from '../../shared/index.js';
22
- import { assertAdmin, validateDocument } from '../../shared/utils.js';
23
+ import { validateDocument } from '../../shared/utils.js';
23
24
  import { createBaseFunction } from '../baseFunction.js';
24
25
  import { CRUD_CONFIG } from '../config/constants.js';
25
26
 
@@ -47,12 +48,10 @@ function createEntityLogicFactory(
47
48
  ) {
48
49
  return async function createEntityLogic(
49
50
  data: CreateEntityRequest,
50
- context: { uid: string; request: CallableRequest<any> }
51
+ context: { uid: string; userRole: UserRole; request: CallableRequest<any> }
51
52
  ) {
52
53
  const { payload, idempotencyKey } = data;
53
-
54
- // Ensure the user is an admin
55
- const uid = await assertAdmin(context.uid);
54
+ const { uid } = context;
56
55
 
57
56
  // Idempotency check if key provided
58
57
  if (idempotencyKey) {
@@ -117,16 +116,14 @@ function createEntityLogicFactory(
117
116
  * Generic function to create entities in any Firestore collection
118
117
  * @param collection - The Firestore collection name
119
118
  * @param documentSchema - The Valibot schema for document validation
119
+ * @param requiredRole - Minimum role required for this operation
120
120
  * @param customSchema - Optional custom request schema
121
121
  * @returns Firebase callable function
122
- *
123
- * @version 0.0.1
124
- * @since 0.0.1
125
- * @author AMBROISE PARK Consulting
126
122
  */
127
123
  export const createEntity = (
128
124
  collection: string,
129
125
  documentSchema: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>,
126
+ requiredRole: UserRole,
130
127
  customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
131
128
  ): CallableFunction<CreateEntityRequest, Promise<any>> => {
132
129
  const requestSchema =
@@ -140,6 +137,7 @@ export const createEntity = (
140
137
  CRUD_CONFIG,
141
138
  requestSchema,
142
139
  'create_entity',
143
- createEntityLogicFactory(collection, documentSchema)
140
+ createEntityLogicFactory(collection, documentSchema),
141
+ requiredRole
144
142
  );
145
143
  };
@@ -9,14 +9,12 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
13
12
  import * as v from 'valibot';
14
13
 
15
- import {
16
- assertAdmin,
17
- DoNotDevError,
18
- findReferences,
19
- } from '../../shared/utils.js';
14
+ import type { UserRole } from '@donotdev/core/server';
15
+ import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
16
+
17
+ import { DoNotDevError, findReferences } from '../../shared/utils.js';
20
18
  import { createBaseFunction } from '../baseFunction.js';
21
19
  import { CRUD_CONFIG } from '../config/constants.js';
22
20
 
@@ -38,13 +36,10 @@ export type DeleteEntityRequest = { id: string };
38
36
  function deleteEntityLogicFactory(collection: string) {
39
37
  return async function deleteEntityLogic(
40
38
  data: DeleteEntityRequest,
41
- context: { uid: string; request: CallableRequest<any> }
39
+ context: { uid: string; userRole: UserRole; request: CallableRequest<any> }
42
40
  ) {
43
41
  const { id } = data;
44
42
 
45
- // Ensure the user is an admin
46
- await assertAdmin(context.uid);
47
-
48
43
  // Check for references to this document
49
44
  const references = await findReferences(collection, id);
50
45
 
@@ -68,6 +63,7 @@ function deleteEntityLogicFactory(collection: string) {
68
63
  /**
69
64
  * Generic function to delete entities from any Firestore collection
70
65
  * @param collection - The Firestore collection name
66
+ * @param requiredRole - Minimum role required for this operation
71
67
  * @param customSchema - Optional custom request schema
72
68
  * @returns Firebase callable function
73
69
  *
@@ -77,6 +73,7 @@ function deleteEntityLogicFactory(collection: string) {
77
73
  */
78
74
  export const deleteEntity = (
79
75
  collection: string,
76
+ requiredRole: UserRole,
80
77
  customSchema?: v.BaseSchema<unknown, any, v.BaseIssue<unknown>>
81
78
  ): CallableFunction<DeleteEntityRequest, Promise<{ success: boolean }>> => {
82
79
  const requestSchema =
@@ -89,6 +86,7 @@ export const deleteEntity = (
89
86
  CRUD_CONFIG,
90
87
  requestSchema,
91
88
  'delete_entity',
92
- deleteEntityLogicFactory(collection)
89
+ deleteEntityLogicFactory(collection),
90
+ requiredRole
93
91
  );
94
92
  };