@donotdev/functions 0.0.10 → 0.0.12

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 (81) hide show
  1. package/README.md +1 -1
  2. package/package.json +32 -8
  3. package/src/firebase/auth/setCustomClaims.ts +26 -4
  4. package/src/firebase/baseFunction.ts +43 -20
  5. package/src/firebase/billing/cancelSubscription.ts +9 -1
  6. package/src/firebase/billing/changePlan.ts +8 -2
  7. package/src/firebase/billing/createCheckoutSession.ts +3 -1
  8. package/src/firebase/billing/createCustomerPortal.ts +16 -2
  9. package/src/firebase/billing/refreshSubscriptionStatus.ts +3 -1
  10. package/src/firebase/billing/webhookHandler.ts +13 -1
  11. package/src/firebase/crud/aggregate.ts +20 -5
  12. package/src/firebase/crud/create.ts +31 -7
  13. package/src/firebase/crud/list.ts +36 -24
  14. package/src/firebase/crud/update.ts +29 -7
  15. package/src/firebase/oauth/exchangeToken.ts +30 -4
  16. package/src/firebase/oauth/githubAccess.ts +8 -3
  17. package/src/firebase/registerCrudFunctions.ts +2 -2
  18. package/src/firebase/scheduled/checkExpiredSubscriptions.ts +20 -3
  19. package/src/shared/__tests__/detectFirestore.test.ts +52 -0
  20. package/src/shared/__tests__/errorHandling.test.ts +144 -0
  21. package/src/shared/__tests__/idempotency.test.ts +95 -0
  22. package/src/shared/__tests__/rateLimiter.test.ts +142 -0
  23. package/src/shared/__tests__/validation.test.ts +172 -0
  24. package/src/shared/billing/__tests__/createCheckout.test.ts +393 -0
  25. package/src/shared/billing/__tests__/webhookHandler.test.ts +1091 -0
  26. package/src/shared/billing/webhookHandler.ts +16 -7
  27. package/src/shared/errorHandling.ts +22 -60
  28. package/src/shared/firebase.ts +1 -25
  29. package/src/shared/index.ts +2 -1
  30. package/src/shared/logger.ts +3 -7
  31. package/src/shared/oauth/__tests__/exchangeToken.test.ts +116 -0
  32. package/src/shared/oauth/__tests__/grantAccess.test.ts +237 -0
  33. package/src/shared/utils/__tests__/functionWrapper.test.ts +122 -0
  34. package/src/shared/utils/external/subscription.ts +12 -2
  35. package/src/shared/utils/internal/auth.ts +140 -16
  36. package/src/shared/utils/internal/rateLimiter.ts +101 -90
  37. package/src/shared/utils/internal/validation.ts +47 -3
  38. package/src/shared/utils.ts +170 -66
  39. package/src/supabase/auth/deleteAccount.ts +52 -0
  40. package/src/supabase/auth/getCustomClaims.ts +56 -0
  41. package/src/supabase/auth/getUserAuthStatus.ts +64 -0
  42. package/src/supabase/auth/removeCustomClaims.ts +75 -0
  43. package/src/supabase/auth/setCustomClaims.ts +73 -0
  44. package/src/supabase/baseFunction.ts +306 -0
  45. package/src/supabase/billing/cancelSubscription.ts +57 -0
  46. package/src/supabase/billing/changePlan.ts +62 -0
  47. package/src/supabase/billing/createCheckoutSession.ts +82 -0
  48. package/src/supabase/billing/createCustomerPortal.ts +58 -0
  49. package/src/supabase/billing/refreshSubscriptionStatus.ts +89 -0
  50. package/src/supabase/crud/aggregate.ts +169 -0
  51. package/src/supabase/crud/create.ts +225 -0
  52. package/src/supabase/crud/delete.ts +154 -0
  53. package/src/supabase/crud/get.ts +89 -0
  54. package/src/supabase/crud/index.ts +24 -0
  55. package/src/supabase/crud/list.ts +388 -0
  56. package/src/supabase/crud/update.ts +199 -0
  57. package/src/supabase/helpers/authProvider.ts +45 -0
  58. package/src/supabase/index.ts +73 -0
  59. package/src/supabase/registerCrudFunctions.ts +180 -0
  60. package/src/supabase/utils/idempotency.ts +141 -0
  61. package/src/supabase/utils/monitoring.ts +187 -0
  62. package/src/supabase/utils/rateLimiter.ts +216 -0
  63. package/src/vercel/api/auth/get-custom-claims.ts +3 -2
  64. package/src/vercel/api/auth/get-user-auth-status.ts +3 -2
  65. package/src/vercel/api/auth/remove-custom-claims.ts +3 -2
  66. package/src/vercel/api/auth/set-custom-claims.ts +5 -2
  67. package/src/vercel/api/billing/cancel.ts +2 -1
  68. package/src/vercel/api/billing/change-plan.ts +3 -1
  69. package/src/vercel/api/billing/customer-portal.ts +4 -1
  70. package/src/vercel/api/billing/refresh-subscription-status.ts +3 -1
  71. package/src/vercel/api/billing/webhook-handler.ts +24 -4
  72. package/src/vercel/api/crud/create.ts +14 -8
  73. package/src/vercel/api/crud/delete.ts +15 -6
  74. package/src/vercel/api/crud/get.ts +16 -8
  75. package/src/vercel/api/crud/list.ts +22 -10
  76. package/src/vercel/api/crud/update.ts +16 -10
  77. package/src/vercel/api/oauth/check-github-access.ts +2 -5
  78. package/src/vercel/api/oauth/grant-github-access.ts +1 -5
  79. package/src/vercel/api/oauth/revoke-github-access.ts +7 -8
  80. package/src/vercel/api/utils/cors.ts +13 -2
  81. package/src/vercel/baseFunction.ts +40 -25
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import Stripe from 'stripe';
13
+ import * as v from 'valibot';
13
14
 
14
15
  import { DoNotDevError } from '@donotdev/core/server';
15
16
  import type { OAuthPartnerId, UserRole } from '@donotdev/core/server';
@@ -19,6 +20,11 @@ import {
19
20
  initFirebaseAdmin,
20
21
  } from '@donotdev/firebase/server';
21
22
 
23
+ import {
24
+ assertAuthenticated as internalAssertAuthenticated,
25
+ assertAdmin as internalAssertAdmin,
26
+ } from './utils/internal/auth.js';
27
+
22
28
  // Re-export DoNotDevError for external use
23
29
  export { DoNotDevError };
24
30
 
@@ -101,17 +107,25 @@ export const stripe = new Proxy({} as Stripe, {
101
107
  });
102
108
 
103
109
  /**
104
- * Assert that a user is authenticated
110
+ * Assert that a user is authenticated from a Firebase callable auth context.
105
111
  *
106
- * @version 0.0.1
112
+ * @deprecated Use `assertAuthenticated` from `shared/utils/internal/auth.js` instead.
113
+ * This wrapper extracts uid from a Firebase callable auth context and delegates
114
+ * to the canonical version.
115
+ *
116
+ * @param auth - Firebase callable request auth context (object with `.uid`)
117
+ * @returns The authenticated user's uid
118
+ *
119
+ * @version 0.0.3
107
120
  * @since 0.0.1
108
121
  * @author AMBROISE PARK Consulting
109
122
  */
110
123
  export function assertAuthenticated(auth: any): string {
111
- if (!auth || !auth.uid) {
124
+ const uid = auth?.uid;
125
+ if (!uid) {
112
126
  throw new Error('User must be authenticated');
113
127
  }
114
- return auth.uid;
128
+ return internalAssertAuthenticated(uid);
115
129
  }
116
130
 
117
131
  /**
@@ -220,68 +234,91 @@ export function createErrorResponse(error: unknown): {
220
234
  }
221
235
 
222
236
  /**
223
- * Updates user subscription in Firebase
224
- * TODO: Implement this function based on your Firebase setup
237
+ * Updates user subscription in Firebase.
225
238
  *
226
- * @version 0.0.1
239
+ * **Architecture decision — throwing stubs for billing functions:**
240
+ * These subscription functions (`updateUserSubscription`, `getUserSubscription`,
241
+ * `cancelUserSubscription`, `handleSubscriptionCancellation`) are intentional
242
+ * placeholder implementations. They exist so the framework compiles and
243
+ * type-checks out of the box, but throw at runtime to surface missing
244
+ * integration early. Consumer apps replace them with their billing provider
245
+ * integration (Stripe, Paddle, LemonSqueezy, etc.) via the documented
246
+ * `shared/billing/helpers/` modules.
247
+ *
248
+ * C10: This was a silent no-op stub. It now throws to surface the missing
249
+ * implementation at startup rather than silently dropping webhook events.
250
+ * Use `updateUserSubscription` from `shared/billing/helpers/updateUserSubscription.ts`
251
+ * (which requires an authProvider) instead.
252
+ *
253
+ * @deprecated Use shared/billing/helpers/updateUserSubscription.ts
254
+ * @version 0.0.2
227
255
  * @since 0.0.1
228
256
  * @author AMBROISE PARK Consulting
229
257
  */
230
258
  export async function updateUserSubscription(
231
- firebaseUid: string,
232
- subscription: any
259
+ _firebaseUid: string,
260
+ _subscription: any
233
261
  ): Promise<void> {
234
- // TODO: Implement subscription update logic
235
- console.log('Updating subscription for user:', firebaseUid, subscription.id);
262
+ throw new DoNotDevError(
263
+ 'updateUserSubscription stub called import from shared/billing/helpers/updateUserSubscription.ts and supply an authProvider',
264
+ 'unimplemented'
265
+ );
236
266
  }
237
267
 
238
268
  /**
239
- * Gets user subscription from Firebase
240
- * TODO: Implement this function based on your Firebase setup
269
+ * Gets user subscription from Firebase.
241
270
  *
242
- * @version 0.0.1
271
+ * C10: This was a silent no-op stub. Now throws to surface the missing implementation.
272
+ *
273
+ * @deprecated Implement directly using Firebase Admin SDK or Stripe API.
274
+ * @version 0.0.2
243
275
  * @since 0.0.1
244
276
  * @author AMBROISE PARK Consulting
245
277
  */
246
- export async function getUserSubscription(firebaseUid: string): Promise<any> {
247
- // TODO: Implement subscription retrieval logic
248
- console.log('Getting subscription for user:', firebaseUid);
249
- return null;
278
+ export async function getUserSubscription(_firebaseUid: string): Promise<any> {
279
+ throw new DoNotDevError(
280
+ 'getUserSubscription stub called — implement using Firebase Admin SDK or Stripe API',
281
+ 'unimplemented'
282
+ );
250
283
  }
251
284
 
252
285
  /**
253
- * Cancels user subscription
254
- * TODO: Implement this function based on your Firebase setup
286
+ * Cancels user subscription.
255
287
  *
256
- * @version 0.0.1
288
+ * C10: This was a silent no-op stub. Now throws to surface the missing implementation.
289
+ *
290
+ * @deprecated Use cancelUserSubscription from shared/billing/helpers/subscriptionManagement.ts
291
+ * @version 0.0.2
257
292
  * @since 0.0.1
258
293
  * @author AMBROISE PARK Consulting
259
294
  */
260
295
  export async function cancelUserSubscription(
261
- firebaseUid: string,
262
- subscription: any
296
+ _firebaseUid: string,
297
+ _subscription: any
263
298
  ): Promise<void> {
264
- // TODO: Implement subscription cancellation logic
265
- console.log('Canceling subscription for user:', firebaseUid, subscription.id);
299
+ throw new DoNotDevError(
300
+ 'cancelUserSubscription stub called import from shared/billing/helpers/subscriptionManagement.ts',
301
+ 'unimplemented'
302
+ );
266
303
  }
267
304
 
268
305
  /**
269
- * Handles subscription cancellation
270
- * TODO: Implement this function based on your Firebase setup
306
+ * Handles subscription cancellation.
271
307
  *
272
- * @version 0.0.1
308
+ * C10: This was a silent no-op stub. Now throws to surface the missing implementation.
309
+ *
310
+ * @deprecated Implement using Stripe webhook events and updateUserSubscription.
311
+ * @version 0.0.2
273
312
  * @since 0.0.1
274
313
  * @author AMBROISE PARK Consulting
275
314
  */
276
315
  export async function handleSubscriptionCancellation(
277
- firebaseUid: string,
278
- subscription: any
316
+ _firebaseUid: string,
317
+ _subscription: any
279
318
  ): Promise<void> {
280
- // TODO: Implement subscription cancellation handling
281
- console.log(
282
- 'Handling subscription cancellation for user:',
283
- firebaseUid,
284
- subscription.id
319
+ throw new DoNotDevError(
320
+ 'handleSubscriptionCancellation stub called — handle via Stripe webhook events',
321
+ 'unimplemented'
285
322
  );
286
323
  }
287
324
 
@@ -289,35 +326,15 @@ export async function handleSubscriptionCancellation(
289
326
  * Asserts that a user has admin privileges
290
327
  * Uses role hierarchy: super > admin > user > guest
291
328
  *
292
- * @version 0.0.2
329
+ * @deprecated Use `assertAdmin` from `shared/utils/internal/auth.js` instead.
330
+ * This is a thin wrapper that delegates to the canonical provider-agnostic version.
331
+ *
332
+ * @version 0.0.3
293
333
  * @since 0.0.1
294
334
  * @author AMBROISE PARK Consulting
295
335
  */
296
336
  export async function assertAdmin(uid: string): Promise<string> {
297
- if (!uid) {
298
- throw new DoNotDevError('Authentication required', 'unauthenticated');
299
- }
300
-
301
- try {
302
- const user = await getFirebaseAdminAuth().getUser(uid);
303
- const claims = user.customClaims || {};
304
-
305
- // Check role claim first (standard pattern)
306
- const role = claims.role;
307
- if (role === 'admin' || role === 'super') {
308
- return uid;
309
- }
310
-
311
- // Fallback: check legacy boolean flags
312
- if (claims.isAdmin === true || claims.isSuper === true) {
313
- return uid;
314
- }
315
-
316
- throw new DoNotDevError('Admin privileges required', 'permission-denied');
317
- } catch (error) {
318
- if (error instanceof DoNotDevError) throw error;
319
- throw new DoNotDevError('Failed to verify admin status', 'internal');
320
- }
337
+ return internalAssertAdmin(uid);
321
338
  }
322
339
 
323
340
  /**
@@ -335,6 +352,54 @@ export function validateDocument(data: any, schema?: any): void {
335
352
  if (Array.isArray(data)) {
336
353
  throw new Error('Document data cannot be an array');
337
354
  }
355
+
356
+ // Run Valibot schema validation when a schema is provided
357
+ if (schema) {
358
+ try {
359
+ v.parse(schema, data);
360
+ } catch (error: any) {
361
+ if (error?.issues) {
362
+ const messages = error.issues
363
+ .map(
364
+ (issue: v.BaseIssue<unknown>) =>
365
+ `${issue.path?.map((p: any) => p.key).join('.') || 'root'}: ${issue.message}`
366
+ )
367
+ .join('; ');
368
+ throw new DoNotDevError(
369
+ `Validation failed: ${messages}`,
370
+ 'invalid-argument',
371
+ { details: { validationErrors: error.issues } }
372
+ );
373
+ }
374
+ throw error;
375
+ }
376
+ }
377
+ }
378
+
379
+ /**
380
+ * Validates a Firestore collection name from client-supplied schema.
381
+ *
382
+ * W22: Vercel CRUD handlers accept `schema` (including collection name) from
383
+ * the client. This is a known design limitation. As a defense-in-depth measure,
384
+ * reject collection names that could be used for path traversal or access to
385
+ * internal collections.
386
+ *
387
+ * @param name - Collection name to validate
388
+ * @throws Error if the name is unsafe
389
+ *
390
+ * @version 0.0.1
391
+ * @since 0.0.1
392
+ * @author AMBROISE PARK Consulting
393
+ */
394
+ export function validateCollectionName(name: string): void {
395
+ if (!name || typeof name !== 'string') {
396
+ throw new Error('Collection name is required');
397
+ }
398
+ if (name.includes('/') || name.includes('..') || name.startsWith('_')) {
399
+ throw new Error(
400
+ 'Invalid collection name: must not contain "/", "..", or start with "_"'
401
+ );
402
+ }
338
403
  }
339
404
 
340
405
  /**
@@ -345,9 +410,16 @@ export function validateDocument(data: any, schema?: any): void {
345
410
  * @author AMBROISE PARK Consulting
346
411
  */
347
412
  export async function verifyFirebaseAuthToken(token: string): Promise<string> {
348
- // TODO: Implement Firebase token verification
349
- // For now, just return a mock uid
350
- return 'mock-uid';
413
+ if (!token) {
414
+ throw new DoNotDevError('Missing authentication token', 'unauthenticated');
415
+ }
416
+
417
+ try {
418
+ const decodedToken = await getFirebaseAdminAuth().verifyIdToken(token);
419
+ return decodedToken.uid;
420
+ } catch (error) {
421
+ throw new DoNotDevError('Invalid or expired token', 'unauthenticated');
422
+ }
351
423
  }
352
424
 
353
425
  /**
@@ -357,7 +429,39 @@ export async function verifyFirebaseAuthToken(token: string): Promise<string> {
357
429
  * @since 0.0.1
358
430
  * @author AMBROISE PARK Consulting
359
431
  */
360
- export function findReferences(collection: string, docId: string): string[] {
361
- // TODO: Implement reference finding logic
362
- return [];
432
+ export async function findReferences(
433
+ collection: string,
434
+ docId: string,
435
+ referenceMetadata?: {
436
+ incoming?: Array<{
437
+ sourceCollection: string;
438
+ sourceField: string;
439
+ }>;
440
+ }
441
+ ): Promise<Array<{ collection: string; field: string; count: number }>> {
442
+ const references: Array<{ collection: string; field: string; count: number }> = [];
443
+
444
+ if (!referenceMetadata?.incoming?.length) {
445
+ return references;
446
+ }
447
+
448
+ const db = getFirebaseAdminFirestore();
449
+
450
+ for (const ref of referenceMetadata.incoming) {
451
+ const snapshot = await db
452
+ .collection(ref.sourceCollection)
453
+ .where(ref.sourceField, '==', docId)
454
+ .limit(1)
455
+ .get();
456
+
457
+ if (!snapshot.empty) {
458
+ references.push({
459
+ collection: ref.sourceCollection,
460
+ field: ref.sourceField,
461
+ count: snapshot.size,
462
+ });
463
+ }
464
+ }
465
+
466
+ return references;
363
467
  }
@@ -0,0 +1,52 @@
1
+ // packages/functions/src/supabase/auth/deleteAccount.ts
2
+
3
+ /**
4
+ * @fileoverview Delete Account Supabase Edge Function
5
+ * @description Deletes the authenticated user's account via Supabase Admin.
6
+ *
7
+ * @version 0.0.1
8
+ * @since 0.5.0
9
+ * @author AMBROISE PARK Consulting
10
+ */
11
+
12
+ import * as v from 'valibot';
13
+
14
+ import { createSupabaseHandler } from '../baseFunction.js';
15
+
16
+ // =============================================================================
17
+ // Schema
18
+ // =============================================================================
19
+
20
+ const deleteAccountSchema = v.object({});
21
+
22
+ // =============================================================================
23
+ // Handler
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Create a Supabase Edge Function handler for account deletion.
28
+ *
29
+ * @returns `(req: Request) => Promise<Response>` handler
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // supabase/functions/delete-account/index.ts
34
+ * import { createDeleteAccount } from '@donotdev/functions/supabase';
35
+ * Deno.serve(createDeleteAccount());
36
+ * ```
37
+ *
38
+ * @version 0.0.1
39
+ * @since 0.5.0
40
+ */
41
+ export function createDeleteAccount() {
42
+ return createSupabaseHandler(
43
+ 'delete-account',
44
+ deleteAccountSchema,
45
+ async (_data, ctx) => {
46
+ const { error } = await ctx.supabaseAdmin.auth.admin.deleteUser(ctx.uid);
47
+ if (error) throw error;
48
+
49
+ return { success: true };
50
+ },
51
+ );
52
+ }
@@ -0,0 +1,56 @@
1
+ // packages/functions/src/supabase/auth/getCustomClaims.ts
2
+
3
+ /**
4
+ * @fileoverview Get Custom Claims Supabase Edge Function
5
+ * @description Returns the authenticated user's app_metadata (custom claims).
6
+ *
7
+ * @version 0.0.1
8
+ * @since 0.6.0
9
+ * @author AMBROISE PARK Consulting
10
+ */
11
+
12
+ import * as v from 'valibot';
13
+
14
+ import { createSupabaseHandler } from '../baseFunction.js';
15
+
16
+ // =============================================================================
17
+ // Schema
18
+ // =============================================================================
19
+
20
+ const getCustomClaimsSchema = v.object({});
21
+
22
+ // =============================================================================
23
+ // Handler
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Create a Supabase Edge Function handler for retrieving custom claims (app_metadata).
28
+ *
29
+ * @returns `(req: Request) => Promise<Response>` handler
30
+ *
31
+ * @example
32
+ * ```typescript
33
+ * // supabase/functions/get-custom-claims/index.ts
34
+ * import { createGetCustomClaims } from '@donotdev/functions/supabase';
35
+ * Deno.serve(createGetCustomClaims());
36
+ * ```
37
+ *
38
+ * @version 0.0.1
39
+ * @since 0.6.0
40
+ */
41
+ export function createGetCustomClaims() {
42
+ return createSupabaseHandler(
43
+ 'get-custom-claims',
44
+ getCustomClaimsSchema,
45
+ async (_data, ctx) => {
46
+ const { data: { user }, error } =
47
+ await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
48
+ if (error || !user) {
49
+ throw new Error('User not found');
50
+ }
51
+
52
+ return { customClaims: user.app_metadata ?? {} };
53
+ },
54
+ 'user',
55
+ );
56
+ }
@@ -0,0 +1,64 @@
1
+ // packages/functions/src/supabase/auth/getUserAuthStatus.ts
2
+
3
+ /**
4
+ * @fileoverview Get User Auth Status Supabase Edge Function
5
+ * @description Returns the authenticated user's profile and custom claims.
6
+ *
7
+ * @version 0.0.1
8
+ * @since 0.6.0
9
+ * @author AMBROISE PARK Consulting
10
+ */
11
+
12
+ import * as v from 'valibot';
13
+
14
+ import { createSupabaseHandler } from '../baseFunction.js';
15
+
16
+ // =============================================================================
17
+ // Schema
18
+ // =============================================================================
19
+
20
+ const getUserAuthStatusSchema = v.object({});
21
+
22
+ // =============================================================================
23
+ // Handler
24
+ // =============================================================================
25
+
26
+ /**
27
+ * Create a Supabase Edge Function handler for retrieving user auth status.
28
+ *
29
+ * Returns uid, email, verification status, custom claims, and ban status.
30
+ *
31
+ * @returns `(req: Request) => Promise<Response>` handler
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * // supabase/functions/get-user-auth-status/index.ts
36
+ * import { createGetUserAuthStatus } from '@donotdev/functions/supabase';
37
+ * Deno.serve(createGetUserAuthStatus());
38
+ * ```
39
+ *
40
+ * @version 0.0.1
41
+ * @since 0.6.0
42
+ */
43
+ export function createGetUserAuthStatus() {
44
+ return createSupabaseHandler(
45
+ 'get-user-auth-status',
46
+ getUserAuthStatusSchema,
47
+ async (_data, ctx) => {
48
+ const { data: { user }, error } =
49
+ await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
50
+ if (error || !user) {
51
+ throw new Error('User not found');
52
+ }
53
+
54
+ return {
55
+ uid: user.id,
56
+ email: user.email,
57
+ emailVerified: user.email_confirmed_at != null,
58
+ customClaims: user.app_metadata ?? {},
59
+ disabled: user.banned_until != null,
60
+ };
61
+ },
62
+ 'user',
63
+ );
64
+ }
@@ -0,0 +1,75 @@
1
+ // packages/functions/src/supabase/auth/removeCustomClaims.ts
2
+
3
+ /**
4
+ * @fileoverview Remove Custom Claims Supabase Edge Function
5
+ * @description Removes specific keys from the user's app_metadata.
6
+ *
7
+ * @version 0.0.1
8
+ * @since 0.6.0
9
+ * @author AMBROISE PARK Consulting
10
+ */
11
+
12
+ import * as v from 'valibot';
13
+
14
+ import { createSupabaseHandler } from '../baseFunction.js';
15
+
16
+ // =============================================================================
17
+ // Schema
18
+ // =============================================================================
19
+
20
+ const removeCustomClaimsSchema = v.object({
21
+ claimsToRemove: v.pipe(
22
+ v.array(v.string()),
23
+ v.minLength(1, 'At least one claim key is required'),
24
+ ),
25
+ });
26
+
27
+ // =============================================================================
28
+ // Handler
29
+ // =============================================================================
30
+
31
+ /**
32
+ * Create a Supabase Edge Function handler for removing custom claims from app_metadata.
33
+ *
34
+ * @returns `(req: Request) => Promise<Response>` handler
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * // supabase/functions/remove-custom-claims/index.ts
39
+ * import { createRemoveCustomClaims } from '@donotdev/functions/supabase';
40
+ * Deno.serve(createRemoveCustomClaims());
41
+ * ```
42
+ *
43
+ * @version 0.0.1
44
+ * @since 0.6.0
45
+ */
46
+ export function createRemoveCustomClaims() {
47
+ return createSupabaseHandler(
48
+ 'remove-custom-claims',
49
+ removeCustomClaimsSchema,
50
+ async (data, ctx) => {
51
+ // Get current app_metadata
52
+ const { data: { user }, error: getUserError } =
53
+ await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
54
+ if (getUserError || !user) {
55
+ throw new Error('User not found');
56
+ }
57
+
58
+ // Remove specified keys
59
+ const existingClaims = { ...(user.app_metadata ?? {}) } as Record<string, unknown>;
60
+ for (const key of data.claimsToRemove) {
61
+ delete existingClaims[key];
62
+ }
63
+
64
+ // Update app_metadata
65
+ const { error: updateError } =
66
+ await ctx.supabaseAdmin.auth.admin.updateUserById(ctx.uid, {
67
+ app_metadata: existingClaims,
68
+ });
69
+ if (updateError) throw updateError;
70
+
71
+ return { success: true, customClaims: existingClaims };
72
+ },
73
+ 'user',
74
+ );
75
+ }
@@ -0,0 +1,73 @@
1
+ // packages/functions/src/supabase/auth/setCustomClaims.ts
2
+
3
+ /**
4
+ * @fileoverview Set Custom Claims Supabase Edge Function
5
+ * @description Merges custom claims into the user's app_metadata.
6
+ *
7
+ * @version 0.0.1
8
+ * @since 0.6.0
9
+ * @author AMBROISE PARK Consulting
10
+ */
11
+
12
+ import * as v from 'valibot';
13
+
14
+ import { createSupabaseHandler } from '../baseFunction.js';
15
+
16
+ // =============================================================================
17
+ // Schema
18
+ // =============================================================================
19
+
20
+ const setCustomClaimsSchema = v.object({
21
+ customClaims: v.record(v.string(), v.unknown()),
22
+ idempotencyKey: v.optional(v.string()),
23
+ });
24
+
25
+ // =============================================================================
26
+ // Handler
27
+ // =============================================================================
28
+
29
+ /**
30
+ * Create a Supabase Edge Function handler for setting custom claims (app_metadata).
31
+ *
32
+ * Merges provided claims into the user's existing app_metadata.
33
+ *
34
+ * @returns `(req: Request) => Promise<Response>` handler
35
+ *
36
+ * @example
37
+ * ```typescript
38
+ * // supabase/functions/set-custom-claims/index.ts
39
+ * import { createSetCustomClaims } from '@donotdev/functions/supabase';
40
+ * Deno.serve(createSetCustomClaims());
41
+ * ```
42
+ *
43
+ * @version 0.0.1
44
+ * @since 0.6.0
45
+ */
46
+ export function createSetCustomClaims() {
47
+ return createSupabaseHandler(
48
+ 'set-custom-claims',
49
+ setCustomClaimsSchema,
50
+ async (data, ctx) => {
51
+ // Get current app_metadata
52
+ const { data: { user }, error: getUserError } =
53
+ await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
54
+ if (getUserError || !user) {
55
+ throw new Error('User not found');
56
+ }
57
+
58
+ // Merge new claims with existing app_metadata
59
+ const existingClaims = (user.app_metadata ?? {}) as Record<string, unknown>;
60
+ const updatedClaims = { ...existingClaims, ...data.customClaims };
61
+
62
+ // Update app_metadata
63
+ const { error: updateError } =
64
+ await ctx.supabaseAdmin.auth.admin.updateUserById(ctx.uid, {
65
+ app_metadata: updatedClaims,
66
+ });
67
+ if (updateError) throw updateError;
68
+
69
+ return { success: true, customClaims: updatedClaims };
70
+ },
71
+ 'user',
72
+ );
73
+ }