@donotdev/functions 0.0.4 → 0.0.6
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 +5 -5
- package/src/firebase/auth/getCustomClaims.ts +13 -4
- package/src/firebase/auth/getUserAuthStatus.ts +21 -4
- package/src/firebase/auth/removeCustomClaims.ts +18 -4
- package/src/firebase/auth/setCustomClaims.ts +18 -4
- package/src/firebase/billing/createCheckoutSession.ts +15 -2
- package/src/firebase/billing/createCustomerPortal.ts +34 -11
- package/src/firebase/billing/webhookHandler.ts +17 -5
- package/src/firebase/config/constants.ts +7 -1
- package/src/firebase/config/secrets.ts +32 -0
- package/src/firebase/crud/aggregate.ts +361 -0
- package/src/firebase/crud/create.ts +22 -4
- package/src/firebase/crud/delete.ts +8 -3
- package/src/firebase/crud/get.ts +16 -4
- package/src/firebase/crud/index.ts +1 -0
- package/src/firebase/crud/list.ts +14 -6
- package/src/firebase/crud/update.ts +21 -4
- package/src/firebase/oauth/githubAccess.ts +27 -6
- package/src/shared/schema.ts +45 -21
- package/src/shared/utils.ts +26 -13
- package/src/vercel/api/crud/create.ts +11 -1
- package/src/vercel/api/crud/get.ts +8 -1
- package/src/vercel/api/crud/list.ts +4 -0
- package/src/vercel/api/crud/update.ts +23 -2
|
@@ -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 =
|
|
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
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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
|
}
|
package/src/shared/schema.ts
CHANGED
|
@@ -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?:
|
|
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
|
|
31
|
+
* @returns The visibility setting if found, otherwise undefined.
|
|
29
32
|
*
|
|
30
|
-
* @version 0.0.
|
|
33
|
+
* @version 0.0.2
|
|
31
34
|
* @since 0.0.1
|
|
32
35
|
* @author AMBROISE PARK Consulting
|
|
33
36
|
*/
|
|
34
|
-
function getFieldVisibility(field: any):
|
|
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
|
|
47
|
+
* and the user's authentication/admin status.
|
|
45
48
|
*
|
|
46
|
-
*
|
|
47
|
-
* -
|
|
48
|
-
* -
|
|
49
|
-
* -
|
|
50
|
-
*
|
|
51
|
-
* -
|
|
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
|
|
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.
|
|
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:
|
|
65
|
-
isAdmin: boolean
|
|
69
|
+
visibility: Visibility | undefined,
|
|
70
|
+
isAdmin: boolean,
|
|
71
|
+
isAuthenticated: boolean = true
|
|
66
72
|
): boolean {
|
|
67
|
-
|
|
68
|
-
|
|
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;
|
|
85
|
+
return isAdmin;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Technical fields are visible only to admins
|
|
89
|
+
if (visibility === 'technical') {
|
|
90
|
+
return isAdmin;
|
|
72
91
|
}
|
|
73
|
-
|
|
74
|
-
//
|
|
75
|
-
|
|
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
|
/**
|
package/src/shared/utils.ts
CHANGED
|
@@ -46,9 +46,24 @@ export function getAuth() {
|
|
|
46
46
|
return getFirebaseAdminAuth();
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
// Lazy initialization of Stripe
|
|
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 (
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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)
|