@donotdev/functions 0.0.9 → 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.
Files changed (80) hide show
  1. package/package.json +31 -7
  2. package/src/firebase/auth/setCustomClaims.ts +26 -4
  3. package/src/firebase/baseFunction.ts +43 -20
  4. package/src/firebase/billing/cancelSubscription.ts +9 -1
  5. package/src/firebase/billing/changePlan.ts +8 -2
  6. package/src/firebase/billing/createCustomerPortal.ts +16 -2
  7. package/src/firebase/billing/refreshSubscriptionStatus.ts +3 -1
  8. package/src/firebase/billing/webhookHandler.ts +13 -1
  9. package/src/firebase/config/constants.ts +0 -3
  10. package/src/firebase/crud/aggregate.ts +20 -5
  11. package/src/firebase/crud/create.ts +31 -7
  12. package/src/firebase/crud/get.ts +16 -8
  13. package/src/firebase/crud/list.ts +70 -29
  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 +15 -4
  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 +16 -54
  28. package/src/shared/firebase.ts +1 -25
  29. package/src/shared/oauth/__tests__/exchangeToken.test.ts +116 -0
  30. package/src/shared/oauth/__tests__/grantAccess.test.ts +237 -0
  31. package/src/shared/schema.ts +7 -1
  32. package/src/shared/utils/__tests__/functionWrapper.test.ts +122 -0
  33. package/src/shared/utils/external/subscription.ts +10 -0
  34. package/src/shared/utils/internal/auth.ts +140 -16
  35. package/src/shared/utils/internal/rateLimiter.ts +101 -90
  36. package/src/shared/utils/internal/validation.ts +47 -3
  37. package/src/shared/utils.ts +154 -39
  38. package/src/supabase/auth/deleteAccount.ts +59 -0
  39. package/src/supabase/auth/getCustomClaims.ts +56 -0
  40. package/src/supabase/auth/getUserAuthStatus.ts +64 -0
  41. package/src/supabase/auth/removeCustomClaims.ts +75 -0
  42. package/src/supabase/auth/setCustomClaims.ts +73 -0
  43. package/src/supabase/baseFunction.ts +302 -0
  44. package/src/supabase/billing/cancelSubscription.ts +57 -0
  45. package/src/supabase/billing/changePlan.ts +62 -0
  46. package/src/supabase/billing/createCheckoutSession.ts +82 -0
  47. package/src/supabase/billing/createCustomerPortal.ts +58 -0
  48. package/src/supabase/billing/refreshSubscriptionStatus.ts +89 -0
  49. package/src/supabase/crud/aggregate.ts +169 -0
  50. package/src/supabase/crud/create.ts +225 -0
  51. package/src/supabase/crud/delete.ts +154 -0
  52. package/src/supabase/crud/get.ts +89 -0
  53. package/src/supabase/crud/index.ts +24 -0
  54. package/src/supabase/crud/list.ts +357 -0
  55. package/src/supabase/crud/update.ts +199 -0
  56. package/src/supabase/helpers/authProvider.ts +45 -0
  57. package/src/supabase/index.ts +73 -0
  58. package/src/supabase/registerCrudFunctions.ts +180 -0
  59. package/src/supabase/utils/idempotency.ts +141 -0
  60. package/src/supabase/utils/monitoring.ts +187 -0
  61. package/src/supabase/utils/rateLimiter.ts +216 -0
  62. package/src/vercel/api/auth/get-custom-claims.ts +3 -2
  63. package/src/vercel/api/auth/get-user-auth-status.ts +3 -2
  64. package/src/vercel/api/auth/remove-custom-claims.ts +3 -2
  65. package/src/vercel/api/auth/set-custom-claims.ts +5 -2
  66. package/src/vercel/api/billing/cancel.ts +2 -1
  67. package/src/vercel/api/billing/change-plan.ts +3 -1
  68. package/src/vercel/api/billing/customer-portal.ts +4 -1
  69. package/src/vercel/api/billing/refresh-subscription-status.ts +3 -1
  70. package/src/vercel/api/billing/webhook-handler.ts +24 -4
  71. package/src/vercel/api/crud/create.ts +14 -8
  72. package/src/vercel/api/crud/delete.ts +15 -6
  73. package/src/vercel/api/crud/get.ts +16 -8
  74. package/src/vercel/api/crud/list.ts +22 -10
  75. package/src/vercel/api/crud/update.ts +16 -10
  76. package/src/vercel/api/oauth/check-github-access.ts +2 -5
  77. package/src/vercel/api/oauth/grant-github-access.ts +1 -5
  78. package/src/vercel/api/oauth/revoke-github-access.ts +7 -8
  79. package/src/vercel/api/utils/cors.ts +13 -2
  80. 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';
@@ -101,9 +102,13 @@ export const stripe = new Proxy({} as Stripe, {
101
102
  });
102
103
 
103
104
  /**
104
- * Assert that a user is authenticated
105
+ * Assert that a user is authenticated from a Firebase callable auth context.
106
+ * For Vercel/Next.js routes, use `verifyAuthToken` from `shared/utils/internal/auth.js` instead.
105
107
  *
106
- * @version 0.0.1
108
+ * @param auth - Firebase callable request auth context (object with `.uid`)
109
+ * @returns The authenticated user's uid
110
+ *
111
+ * @version 0.0.2
107
112
  * @since 0.0.1
108
113
  * @author AMBROISE PARK Consulting
109
114
  */
@@ -220,68 +225,91 @@ export function createErrorResponse(error: unknown): {
220
225
  }
221
226
 
222
227
  /**
223
- * Updates user subscription in Firebase
224
- * TODO: Implement this function based on your Firebase setup
228
+ * Updates user subscription in Firebase.
225
229
  *
226
- * @version 0.0.1
230
+ * **Architecture decision — throwing stubs for billing functions:**
231
+ * These subscription functions (`updateUserSubscription`, `getUserSubscription`,
232
+ * `cancelUserSubscription`, `handleSubscriptionCancellation`) are intentional
233
+ * placeholder implementations. They exist so the framework compiles and
234
+ * type-checks out of the box, but throw at runtime to surface missing
235
+ * integration early. Consumer apps replace them with their billing provider
236
+ * integration (Stripe, Paddle, LemonSqueezy, etc.) via the documented
237
+ * `shared/billing/helpers/` modules.
238
+ *
239
+ * C10: This was a silent no-op stub. It now throws to surface the missing
240
+ * implementation at startup rather than silently dropping webhook events.
241
+ * Use `updateUserSubscription` from `shared/billing/helpers/updateUserSubscription.ts`
242
+ * (which requires an authProvider) instead.
243
+ *
244
+ * @deprecated Use shared/billing/helpers/updateUserSubscription.ts
245
+ * @version 0.0.2
227
246
  * @since 0.0.1
228
247
  * @author AMBROISE PARK Consulting
229
248
  */
230
249
  export async function updateUserSubscription(
231
- firebaseUid: string,
232
- subscription: any
250
+ _firebaseUid: string,
251
+ _subscription: any
233
252
  ): Promise<void> {
234
- // TODO: Implement subscription update logic
235
- console.log('Updating subscription for user:', firebaseUid, subscription.id);
253
+ throw new DoNotDevError(
254
+ 'updateUserSubscription stub called import from shared/billing/helpers/updateUserSubscription.ts and supply an authProvider',
255
+ 'unimplemented'
256
+ );
236
257
  }
237
258
 
238
259
  /**
239
- * Gets user subscription from Firebase
240
- * TODO: Implement this function based on your Firebase setup
260
+ * Gets user subscription from Firebase.
241
261
  *
242
- * @version 0.0.1
262
+ * C10: This was a silent no-op stub. Now throws to surface the missing implementation.
263
+ *
264
+ * @deprecated Implement directly using Firebase Admin SDK or Stripe API.
265
+ * @version 0.0.2
243
266
  * @since 0.0.1
244
267
  * @author AMBROISE PARK Consulting
245
268
  */
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;
269
+ export async function getUserSubscription(_firebaseUid: string): Promise<any> {
270
+ throw new DoNotDevError(
271
+ 'getUserSubscription stub called — implement using Firebase Admin SDK or Stripe API',
272
+ 'unimplemented'
273
+ );
250
274
  }
251
275
 
252
276
  /**
253
- * Cancels user subscription
254
- * TODO: Implement this function based on your Firebase setup
277
+ * Cancels user subscription.
255
278
  *
256
- * @version 0.0.1
279
+ * C10: This was a silent no-op stub. Now throws to surface the missing implementation.
280
+ *
281
+ * @deprecated Use cancelUserSubscription from shared/billing/helpers/subscriptionManagement.ts
282
+ * @version 0.0.2
257
283
  * @since 0.0.1
258
284
  * @author AMBROISE PARK Consulting
259
285
  */
260
286
  export async function cancelUserSubscription(
261
- firebaseUid: string,
262
- subscription: any
287
+ _firebaseUid: string,
288
+ _subscription: any
263
289
  ): Promise<void> {
264
- // TODO: Implement subscription cancellation logic
265
- console.log('Canceling subscription for user:', firebaseUid, subscription.id);
290
+ throw new DoNotDevError(
291
+ 'cancelUserSubscription stub called import from shared/billing/helpers/subscriptionManagement.ts',
292
+ 'unimplemented'
293
+ );
266
294
  }
267
295
 
268
296
  /**
269
- * Handles subscription cancellation
270
- * TODO: Implement this function based on your Firebase setup
297
+ * Handles subscription cancellation.
271
298
  *
272
- * @version 0.0.1
299
+ * C10: This was a silent no-op stub. Now throws to surface the missing implementation.
300
+ *
301
+ * @deprecated Implement using Stripe webhook events and updateUserSubscription.
302
+ * @version 0.0.2
273
303
  * @since 0.0.1
274
304
  * @author AMBROISE PARK Consulting
275
305
  */
276
306
  export async function handleSubscriptionCancellation(
277
- firebaseUid: string,
278
- subscription: any
307
+ _firebaseUid: string,
308
+ _subscription: any
279
309
  ): Promise<void> {
280
- // TODO: Implement subscription cancellation handling
281
- console.log(
282
- 'Handling subscription cancellation for user:',
283
- firebaseUid,
284
- subscription.id
310
+ throw new DoNotDevError(
311
+ 'handleSubscriptionCancellation stub called — handle via Stripe webhook events',
312
+ 'unimplemented'
285
313
  );
286
314
  }
287
315
 
@@ -335,6 +363,54 @@ export function validateDocument(data: any, schema?: any): void {
335
363
  if (Array.isArray(data)) {
336
364
  throw new Error('Document data cannot be an array');
337
365
  }
366
+
367
+ // Run Valibot schema validation when a schema is provided
368
+ if (schema) {
369
+ try {
370
+ v.parse(schema, data);
371
+ } catch (error: any) {
372
+ if (error?.issues) {
373
+ const messages = error.issues
374
+ .map(
375
+ (issue: v.BaseIssue<unknown>) =>
376
+ `${issue.path?.map((p: any) => p.key).join('.') || 'root'}: ${issue.message}`
377
+ )
378
+ .join('; ');
379
+ throw new DoNotDevError(
380
+ `Validation failed: ${messages}`,
381
+ 'invalid-argument',
382
+ { details: { validationErrors: error.issues } }
383
+ );
384
+ }
385
+ throw error;
386
+ }
387
+ }
388
+ }
389
+
390
+ /**
391
+ * Validates a Firestore collection name from client-supplied schema.
392
+ *
393
+ * W22: Vercel CRUD handlers accept `schema` (including collection name) from
394
+ * the client. This is a known design limitation. As a defense-in-depth measure,
395
+ * reject collection names that could be used for path traversal or access to
396
+ * internal collections.
397
+ *
398
+ * @param name - Collection name to validate
399
+ * @throws Error if the name is unsafe
400
+ *
401
+ * @version 0.0.1
402
+ * @since 0.0.1
403
+ * @author AMBROISE PARK Consulting
404
+ */
405
+ export function validateCollectionName(name: string): void {
406
+ if (!name || typeof name !== 'string') {
407
+ throw new Error('Collection name is required');
408
+ }
409
+ if (name.includes('/') || name.includes('..') || name.startsWith('_')) {
410
+ throw new Error(
411
+ 'Invalid collection name: must not contain "/", "..", or start with "_"'
412
+ );
413
+ }
338
414
  }
339
415
 
340
416
  /**
@@ -345,9 +421,16 @@ export function validateDocument(data: any, schema?: any): void {
345
421
  * @author AMBROISE PARK Consulting
346
422
  */
347
423
  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';
424
+ if (!token) {
425
+ throw new DoNotDevError('Missing authentication token', 'unauthenticated');
426
+ }
427
+
428
+ try {
429
+ const decodedToken = await getFirebaseAdminAuth().verifyIdToken(token);
430
+ return decodedToken.uid;
431
+ } catch (error) {
432
+ throw new DoNotDevError('Invalid or expired token', 'unauthenticated');
433
+ }
351
434
  }
352
435
 
353
436
  /**
@@ -357,7 +440,39 @@ export async function verifyFirebaseAuthToken(token: string): Promise<string> {
357
440
  * @since 0.0.1
358
441
  * @author AMBROISE PARK Consulting
359
442
  */
360
- export function findReferences(collection: string, docId: string): string[] {
361
- // TODO: Implement reference finding logic
362
- return [];
443
+ export async function findReferences(
444
+ collection: string,
445
+ docId: string,
446
+ referenceMetadata?: {
447
+ incoming?: Array<{
448
+ sourceCollection: string;
449
+ sourceField: string;
450
+ }>;
451
+ }
452
+ ): Promise<Array<{ collection: string; field: string; count: number }>> {
453
+ const references: Array<{ collection: string; field: string; count: number }> = [];
454
+
455
+ if (!referenceMetadata?.incoming?.length) {
456
+ return references;
457
+ }
458
+
459
+ const db = getFirebaseAdminFirestore();
460
+
461
+ for (const ref of referenceMetadata.incoming) {
462
+ const snapshot = await db
463
+ .collection(ref.sourceCollection)
464
+ .where(ref.sourceField, '==', docId)
465
+ .limit(1)
466
+ .get();
467
+
468
+ if (!snapshot.empty) {
469
+ references.push({
470
+ collection: ref.sourceCollection,
471
+ field: ref.sourceField,
472
+ count: snapshot.size,
473
+ });
474
+ }
475
+ }
476
+
477
+ return references;
363
478
  }
@@ -0,0 +1,59 @@
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
+ userId: v.pipe(v.string(), v.minLength(1, 'User ID is required')),
22
+ });
23
+
24
+ // =============================================================================
25
+ // Handler
26
+ // =============================================================================
27
+
28
+ /**
29
+ * Create a Supabase Edge Function handler for account deletion.
30
+ *
31
+ * @returns `(req: Request) => Promise<Response>` handler
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * // supabase/functions/delete-account/index.ts
36
+ * import { createDeleteAccount } from '@donotdev/functions/supabase';
37
+ * Deno.serve(createDeleteAccount());
38
+ * ```
39
+ *
40
+ * @version 0.0.1
41
+ * @since 0.5.0
42
+ */
43
+ export function createDeleteAccount() {
44
+ return createSupabaseHandler(
45
+ 'delete-account',
46
+ deleteAccountSchema,
47
+ async (data, ctx) => {
48
+ // Guard: users can only delete their own account
49
+ if (data.userId !== ctx.uid) {
50
+ throw new Error('Forbidden: cannot delete another user');
51
+ }
52
+
53
+ const { error } = await ctx.supabaseAdmin.auth.admin.deleteUser(data.userId);
54
+ if (error) throw error;
55
+
56
+ return { success: true };
57
+ },
58
+ );
59
+ }
@@ -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
+ }