@donotdev/functions 0.0.3 → 0.0.5

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.
@@ -26,6 +26,15 @@ import {
26
26
  import { handleError } from '../../shared/errorHandling.js';
27
27
  import { GitHubApiService } from '../../shared/index.js';
28
28
  import { assertAuthenticated } from '../../shared/utils.js';
29
+ import { AUTH_CONFIG } from '../config/constants.js';
30
+ import { githubPersonalAccessToken } from '../config/secrets.js';
31
+
32
+ import type { CallableFunction } from 'firebase-functions/v2/https';
33
+
34
+ const OAUTH_CONFIG = {
35
+ ...AUTH_CONFIG,
36
+ secrets: [githubPersonalAccessToken],
37
+ };
29
38
 
30
39
  /**
31
40
  * Internal function that accepts custom schema
@@ -66,7 +75,7 @@ async function grantGitHubAccessInternal(
66
75
  } = validatedData;
67
76
 
68
77
  // Get GitHub token
69
- const githubToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
78
+ const githubToken = githubPersonalAccessToken.value();
70
79
  if (!githubToken) {
71
80
  throw handleError(
72
81
  new Error('GitHub Personal Access Token not configured')
@@ -139,7 +148,11 @@ async function grantGitHubAccessInternal(
139
148
  * @since 0.0.1
140
149
  * @author AMBROISE PARK Consulting
141
150
  */
142
- export const grantGitHubAccess = onCall<GrantGitHubAccessRequest>(
151
+ export const grantGitHubAccess: CallableFunction<
152
+ GrantGitHubAccessRequest,
153
+ Promise<any>
154
+ > = onCall<GrantGitHubAccessRequest>(
155
+ OAUTH_CONFIG,
143
156
  async (request: CallableRequest<GrantGitHubAccessRequest>) => {
144
157
  return await grantGitHubAccessInternal(request);
145
158
  }
@@ -195,7 +208,7 @@ async function revokeGitHubAccessInternal(
195
208
  const { userId, githubUsername, repoConfig } = validationResult.output;
196
209
 
197
210
  // Get GitHub token
198
- const githubToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
211
+ const githubToken = githubPersonalAccessToken.value();
199
212
  if (!githubToken) {
200
213
  throw handleError(
201
214
  new Error('GitHub Personal Access Token not configured')
@@ -264,7 +277,11 @@ async function revokeGitHubAccessInternal(
264
277
  * @since 0.0.1
265
278
  * @author AMBROISE PARK Consulting
266
279
  */
267
- export const revokeGitHubAccess = onCall<RevokeGitHubAccessRequest>(
280
+ export const revokeGitHubAccess: CallableFunction<
281
+ RevokeGitHubAccessRequest,
282
+ Promise<any>
283
+ > = onCall<RevokeGitHubAccessRequest>(
284
+ OAUTH_CONFIG,
268
285
  async (request: CallableRequest<RevokeGitHubAccessRequest>) => {
269
286
  return await revokeGitHubAccessInternal(request);
270
287
  }
@@ -320,7 +337,7 @@ async function checkGitHubAccessInternal(
320
337
  const { userId, githubUsername, repoConfig } = validationResult.output;
321
338
 
322
339
  // Get GitHub token
323
- const githubToken = process.env.GITHUB_PERSONAL_ACCESS_TOKEN;
340
+ const githubToken = githubPersonalAccessToken.value();
324
341
  if (!githubToken) {
325
342
  throw handleError(
326
343
  new Error('GitHub Personal Access Token not configured')
@@ -369,7 +386,11 @@ async function checkGitHubAccessInternal(
369
386
  * @since 0.0.1
370
387
  * @author AMBROISE PARK Consulting
371
388
  */
372
- export const checkGitHubAccess = onCall<CheckGitHubAccessRequest>(
389
+ export const checkGitHubAccess: CallableFunction<
390
+ CheckGitHubAccessRequest,
391
+ Promise<any>
392
+ > = onCall<CheckGitHubAccessRequest>(
393
+ OAUTH_CONFIG,
373
394
  async (request: CallableRequest<CheckGitHubAccessRequest>) => {
374
395
  return await checkGitHubAccessInternal(request);
375
396
  }
@@ -11,13 +11,16 @@
11
11
 
12
12
  import * as v from 'valibot';
13
13
 
14
+ /** Visibility levels matching @donotdev/core */
15
+ type Visibility = 'guest' | 'user' | 'admin' | 'technical' | 'hidden';
16
+
14
17
  // Define a type for the custom visibility property we might add to Valibot schemas
15
18
  interface ValibotSchemaWithVisibility extends v.BaseSchema<
16
19
  unknown,
17
20
  any,
18
21
  v.BaseIssue<unknown>
19
22
  > {
20
- visibility?: 'public' | 'admin';
23
+ visibility?: Visibility;
21
24
  }
22
25
 
23
26
  /**
@@ -25,13 +28,13 @@ interface ValibotSchemaWithVisibility extends v.BaseSchema<
25
28
  * Looks for a custom 'visibility' property on the schema object.
26
29
  *
27
30
  * @param field - A Valibot schema field (e.g., v.string()).
28
- * @returns The visibility setting ('public' or 'admin') if found, otherwise undefined.
31
+ * @returns The visibility setting if found, otherwise undefined.
29
32
  *
30
- * @version 0.0.1
33
+ * @version 0.0.2
31
34
  * @since 0.0.1
32
35
  * @author AMBROISE PARK Consulting
33
36
  */
34
- function getFieldVisibility(field: any): 'public' | 'admin' | undefined {
37
+ function getFieldVisibility(field: any): Visibility | undefined {
35
38
  // Check if the field has visibility property
36
39
  if (field && typeof field === 'object' && 'visibility' in field) {
37
40
  return (field as ValibotSchemaWithVisibility).visibility;
@@ -41,38 +44,59 @@ function getFieldVisibility(field: any): 'public' | 'admin' | undefined {
41
44
 
42
45
  /**
43
46
  * Determines if a field should be visible based on its visibility setting
44
- * and the user's administrative status.
47
+ * and the user's authentication/admin status.
45
48
  *
46
- * Rules:
47
- * - If visibility is 'public', it's always visible.
48
- * - If visibility is 'admin', it's visible only if isAdmin is true.
49
- * - If visibility is undefined (not set), the field is considered internal/technical
50
- * and is visible only to admins AND if its key starts with an underscore '_'.
51
- * - Fields without an underscore prefix and undefined visibility are hidden by default.
49
+ * Visibility levels:
50
+ * - 'guest': Always visible (even to unauthenticated users)
51
+ * - 'user': Visible to authenticated users (users see both guest and user fields)
52
+ * - 'admin': Visible only to admins
53
+ * - 'technical': Visible to admins only (shown as read-only in edit forms)
54
+ * - 'hidden': Never visible (passwords, tokens, API keys - only in DB)
55
+ * - undefined: Defaults to 'user' behavior (visible to authenticated users)
52
56
  *
53
57
  * @param key - The name (key) of the field.
54
- * @param visibility - The visibility setting ('public', 'admin', or undefined).
58
+ * @param visibility - The visibility setting.
55
59
  * @param isAdmin - Whether the current user is an admin.
60
+ * @param isAuthenticated - Whether the current user is authenticated (defaults to true for backward compat).
56
61
  * @returns True if the field should be visible, false otherwise.
57
62
  *
58
- * @version 0.0.1
63
+ * @version 0.0.2
59
64
  * @since 0.0.1
60
65
  * @author AMBROISE PARK Consulting
61
66
  */
62
67
  export function isFieldVisible(
63
68
  key: string,
64
- visibility: 'public' | 'admin' | undefined,
65
- isAdmin: boolean
69
+ visibility: Visibility | undefined,
70
+ isAdmin: boolean,
71
+ isAuthenticated: boolean = true
66
72
  ): boolean {
67
- if (visibility === 'public') {
68
- return true; // Public fields are always visible
73
+ // Hidden fields are never exposed
74
+ if (visibility === 'hidden') {
75
+ return false;
76
+ }
77
+
78
+ // Guest fields are always visible
79
+ if (visibility === 'guest') {
80
+ return true;
69
81
  }
82
+
83
+ // Admin fields are visible only to admins
70
84
  if (visibility === 'admin') {
71
- return isAdmin; // Admin fields are visible only to admins
85
+ return isAdmin;
86
+ }
87
+
88
+ // Technical fields are visible only to admins
89
+ if (visibility === 'technical') {
90
+ return isAdmin;
72
91
  }
73
- // If visibility is undefined:
74
- // Only show if it's an admin AND the key starts with '_' (internal field convention)
75
- return isAdmin && key.startsWith('_');
92
+
93
+ // User fields (or undefined/default) are visible to authenticated users
94
+ if (visibility === 'user' || visibility === undefined) {
95
+ return isAuthenticated;
96
+ }
97
+
98
+ // Fallback: hide unknown visibility levels
99
+ return false;
76
100
  }
77
101
 
78
102
  /**
@@ -46,9 +46,24 @@ export function getAuth() {
46
46
  return getFirebaseAdminAuth();
47
47
  }
48
48
 
49
- // Lazy initialization of Stripe to avoid errors during Firebase analysis
49
+ // Lazy initialization of Stripe
50
50
  let _stripe: Stripe | null = null;
51
51
 
52
+ /**
53
+ * Initialize Stripe explicitly (e.g. with a secret from defineSecret)
54
+ * @param apiKey - Stripe secret key
55
+ */
56
+ export function initStripe(apiKey: string) {
57
+ if (!apiKey) throw new Error('Stripe API key is required');
58
+
59
+ const apiVersion = process.env.STRIPE_API_VERSION || '2024-12-18.acacia'; // Fallback or strict latest
60
+
61
+ _stripe = new Stripe(apiKey, {
62
+ apiVersion: apiVersion as any,
63
+ });
64
+ return _stripe;
65
+ }
66
+
52
67
  /**
53
68
  * Get Stripe instance with lazy initialization
54
69
  *
@@ -58,21 +73,18 @@ let _stripe: Stripe | null = null;
58
73
  */
59
74
  export function getStripe(): Stripe {
60
75
  if (!_stripe) {
76
+ // Legacy/Vercel fallback: Try process.env
61
77
  const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
62
- if (!STRIPE_SECRET_KEY) {
63
- throw new Error('STRIPE_SECRET_KEY environment variable is required');
64
- }
65
-
66
- const STRIPE_API_VERSION = process.env.STRIPE_API_VERSION;
67
- if (!STRIPE_API_VERSION) {
68
- throw new Error('STRIPE_API_VERSION environment variable is required');
78
+ if (STRIPE_SECRET_KEY) {
79
+ initStripe(STRIPE_SECRET_KEY);
80
+ } else {
81
+ throw new Error(
82
+ 'Stripe not initialized. Call initStripe() or set STRIPE_SECRET_KEY env var.'
83
+ );
69
84
  }
70
-
71
- _stripe = new Stripe(STRIPE_SECRET_KEY, {
72
- apiVersion: STRIPE_API_VERSION as any,
73
- });
74
85
  }
75
- return _stripe;
86
+ // We know _stripe is set here or we threw
87
+ return _stripe!;
76
88
  }
77
89
 
78
90
  /**
@@ -110,6 +122,7 @@ export function assertAuthenticated(auth: any): string {
110
122
  * @author AMBROISE PARK Consulting
111
123
  */
112
124
  export function validateStripeEnvironment(): void {
125
+ // Functions v2 automatically injects secrets as process.env when declared in config
113
126
  const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY;
114
127
  if (!STRIPE_SECRET_KEY) {
115
128
  throw new Error('Missing STRIPE_SECRET_KEY environment variable');
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { CreateEntityData } from '@donotdev/core/server';
13
+ import { DEFAULT_STATUS_VALUE } from '@donotdev/core/server';
13
14
 
14
15
  import { handleError } from '../../../shared/errorHandling.js';
15
16
  import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
@@ -44,12 +45,21 @@ export default async function handler(
44
45
  throw handleError(new Error('Missing schema or payload'));
45
46
  }
46
47
 
48
+ // Determine status (default to draft if not provided)
49
+ const status = payload.status ?? DEFAULT_STATUS_VALUE;
50
+ const isDraft = status === 'draft';
51
+
47
52
  // Validate the document against the schema
48
- validateDocument(payload as Record<string, any>, schema);
53
+ // Skip validation for drafts - required fields can be incomplete
54
+ if (!isDraft) {
55
+ validateDocument(payload as Record<string, any>, schema);
56
+ }
49
57
 
50
58
  // Prepare the document for Firestore and add metadata
59
+ // Always ensure status is set
51
60
  const documentData = {
52
61
  ...prepareForFirestore(payload),
62
+ status, // Ensure status is always present
53
63
  ...createMetadata(uid),
54
64
  };
55
65
 
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { GetEntityData } from '@donotdev/core/server';
13
+ import { HIDDEN_STATUSES } from '@donotdev/core/server';
13
14
 
14
15
  import { handleError } from '../../../shared/errorHandling.js';
15
16
  import { transformFirestoreData } from '../../../shared/index.js';
@@ -49,10 +50,16 @@ export default async function handler(
49
50
  throw handleError(new Error('Document not found'));
50
51
  }
51
52
 
53
+ // Hide drafts/deleted (Vercel routes treat all requests as non-admin)
54
+ const docData = doc.data();
55
+ if ((HIDDEN_STATUSES as readonly string[]).includes(docData?.status)) {
56
+ throw handleError(new Error('Document not found'));
57
+ }
58
+
52
59
  // Transform the document data
53
60
  const documentData = transformFirestoreData({
54
61
  id: doc.id,
55
- ...doc.data(),
62
+ ...docData,
56
63
  });
57
64
 
58
65
  // Filter visible fields based on schema
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { ListEntityData } from '@donotdev/core/server';
13
+ import { HIDDEN_STATUSES } from '@donotdev/core/server';
13
14
 
14
15
  import { handleError } from '../../../shared/errorHandling.js';
15
16
  import { transformFirestoreData } from '../../../shared/index.js';
@@ -50,6 +51,9 @@ export default async function handler(
50
51
  let query: FirebaseFirestore.Query<FirebaseFirestore.DocumentData> =
51
52
  db.collection(schema.metadata.collection);
52
53
 
54
+ // Filter out hidden statuses (Vercel routes treat all requests as non-admin)
55
+ query = query.where('status', 'not-in', [...HIDDEN_STATUSES]);
56
+
53
57
  // Apply limit and offset
54
58
  query = query.limit(parseInt(limit as string));
55
59
  query = query.offset(parseInt(offset as string));
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import type { UpdateEntityData } from '@donotdev/core/server';
13
+ import { DEFAULT_STATUS_VALUE } from '@donotdev/core/server';
13
14
 
14
15
  import { handleError } from '../../../shared/errorHandling.js';
15
16
  import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
@@ -44,8 +45,29 @@ export default async function handler(
44
45
  throw handleError(new Error('Missing schema, id, or payload'));
45
46
  }
46
47
 
48
+ const db = getFirebaseAdminFirestore();
49
+
50
+ // Get current document to merge with payload for status check
51
+ const currentDoc = await db
52
+ .collection(schema.metadata.collection)
53
+ .doc(id)
54
+ .get();
55
+
56
+ if (!currentDoc.exists) {
57
+ throw handleError(new Error('Document not found'));
58
+ }
59
+
60
+ // Merge current data with payload to determine resulting status
61
+ const currentData = currentDoc.data() || {};
62
+ const mergedData = { ...currentData, ...payload };
63
+ const resultingStatus = mergedData.status ?? DEFAULT_STATUS_VALUE;
64
+ const isDraft = resultingStatus === 'draft';
65
+
47
66
  // Validate the document against the schema
48
- validateDocument(payload as Record<string, any>, schema);
67
+ // Skip validation for drafts - required fields can be incomplete
68
+ if (!isDraft) {
69
+ validateDocument(mergedData as Record<string, any>, schema);
70
+ }
49
71
 
50
72
  // Prepare the document for Firestore and add metadata
51
73
  const documentData = {
@@ -54,7 +76,6 @@ export default async function handler(
54
76
  };
55
77
 
56
78
  // Update the document in Firestore
57
- const db = getFirebaseAdminFirestore();
58
79
  await db
59
80
  .collection(schema.metadata.collection)
60
81
  .doc(id)