@donotdev/functions 0.0.11 → 0.0.13

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 (55) hide show
  1. package/README.md +1 -1
  2. package/package.json +9 -9
  3. package/src/firebase/auth/setCustomClaims.ts +19 -5
  4. package/src/firebase/baseFunction.ts +11 -3
  5. package/src/firebase/billing/changePlan.ts +5 -1
  6. package/src/firebase/billing/createCheckoutSession.ts +3 -1
  7. package/src/firebase/billing/createCustomerPortal.ts +6 -2
  8. package/src/firebase/billing/webhookHandler.ts +4 -1
  9. package/src/firebase/crud/aggregate.ts +5 -1
  10. package/src/firebase/crud/create.ts +17 -4
  11. package/src/firebase/crud/list.ts +37 -5
  12. package/src/firebase/crud/update.ts +17 -4
  13. package/src/firebase/oauth/exchangeToken.ts +17 -4
  14. package/src/shared/__tests__/validation.test.ts +5 -3
  15. package/src/shared/billing/__tests__/createCheckout.test.ts +123 -22
  16. package/src/shared/billing/__tests__/webhookHandler.test.ts +121 -30
  17. package/src/shared/errorHandling.ts +6 -6
  18. package/src/shared/firebase.ts +1 -1
  19. package/src/shared/index.ts +2 -1
  20. package/src/shared/logger.ts +9 -7
  21. package/src/shared/oauth/__tests__/exchangeToken.test.ts +6 -8
  22. package/src/shared/oauth/__tests__/grantAccess.test.ts +31 -14
  23. package/src/shared/utils/external/subscription.ts +2 -2
  24. package/src/shared/utils/internal/auth.ts +10 -3
  25. package/src/shared/utils/internal/rateLimiter.ts +8 -2
  26. package/src/shared/utils.ts +23 -30
  27. package/src/supabase/auth/deleteAccount.ts +4 -11
  28. package/src/supabase/auth/getCustomClaims.ts +5 -3
  29. package/src/supabase/auth/getUserAuthStatus.ts +5 -3
  30. package/src/supabase/auth/removeCustomClaims.ts +10 -5
  31. package/src/supabase/auth/setCustomClaims.ts +9 -4
  32. package/src/supabase/baseFunction.ts +80 -21
  33. package/src/supabase/billing/cancelSubscription.ts +9 -3
  34. package/src/supabase/billing/changePlan.ts +20 -5
  35. package/src/supabase/billing/createCheckoutSession.ts +20 -5
  36. package/src/supabase/billing/createCustomerPortal.ts +14 -4
  37. package/src/supabase/billing/refreshSubscriptionStatus.ts +29 -9
  38. package/src/supabase/crud/aggregate.ts +14 -4
  39. package/src/supabase/crud/create.ts +30 -11
  40. package/src/supabase/crud/delete.ts +11 -3
  41. package/src/supabase/crud/get.ts +25 -3
  42. package/src/supabase/crud/list.ts +106 -21
  43. package/src/supabase/crud/update.ts +32 -10
  44. package/src/supabase/helpers/authProvider.ts +5 -2
  45. package/src/supabase/index.ts +1 -4
  46. package/src/supabase/registerCrudFunctions.ts +11 -9
  47. package/src/supabase/utils/idempotency.ts +13 -15
  48. package/src/supabase/utils/monitoring.ts +5 -1
  49. package/src/supabase/utils/rateLimiter.ts +13 -3
  50. package/src/vercel/api/billing/webhook-handler.ts +6 -2
  51. package/src/vercel/api/crud/create.ts +7 -2
  52. package/src/vercel/api/crud/delete.ts +3 -1
  53. package/src/vercel/api/crud/get.ts +3 -1
  54. package/src/vercel/api/crud/list.ts +3 -1
  55. package/src/vercel/api/crud/update.ts +7 -2
@@ -11,11 +11,11 @@
11
11
 
12
12
  import { describe, it, expect, vi } from 'vitest';
13
13
 
14
- import {
15
- exchangeTokenAlgorithm,
16
- type OAuthProvider,
17
- } from '../exchangeToken';
18
- import type { ExchangeTokenRequest, TokenResponse } from '@donotdev/core/server';
14
+ import { exchangeTokenAlgorithm, type OAuthProvider } from '../exchangeToken';
15
+ import type {
16
+ ExchangeTokenRequest,
17
+ TokenResponse,
18
+ } from '@donotdev/core/server';
19
19
 
20
20
  // ---------------------------------------------------------------------------
21
21
  // Helpers
@@ -94,9 +94,7 @@ describe('exchangeTokenAlgorithm', () => {
94
94
 
95
95
  describe('error scenarios', () => {
96
96
  it('re-throws errors from provider', async () => {
97
- const oauthProvider = makeRejectingProvider(
98
- new Error('invalid_grant')
99
- );
97
+ const oauthProvider = makeRejectingProvider(new Error('invalid_grant'));
100
98
 
101
99
  await expect(
102
100
  exchangeTokenAlgorithm(BASE_REQUEST, oauthProvider)
@@ -1,17 +1,15 @@
1
1
  import { describe, it, expect, vi, beforeEach } from 'vitest';
2
2
 
3
- import {
4
- grantAccessAlgorithm,
5
- type OAuthGrantProvider,
6
- } from '../grantAccess';
3
+ import { grantAccessAlgorithm, type OAuthGrantProvider } from '../grantAccess';
7
4
 
8
5
  // ---------------------------------------------------------------------------
9
6
  // Helpers
10
7
  // ---------------------------------------------------------------------------
11
8
 
12
- function makeProvider(
13
- result: { success: boolean; message: string }
14
- ): OAuthGrantProvider {
9
+ function makeProvider(result: {
10
+ success: boolean;
11
+ message: string;
12
+ }): OAuthGrantProvider {
15
13
  return { grantAccess: vi.fn().mockResolvedValue(result) };
16
14
  }
17
15
 
@@ -187,7 +185,10 @@ describe('grantAccessAlgorithm', () => {
187
185
  );
188
186
 
189
187
  expect(oauthProvider.grantAccess).toHaveBeenCalledWith(
190
- expect.objectContaining({ provider: providerName, accessToken: token })
188
+ expect.objectContaining({
189
+ provider: providerName,
190
+ accessToken: token,
191
+ })
191
192
  );
192
193
  }
193
194
  );
@@ -199,12 +200,16 @@ describe('grantAccessAlgorithm', () => {
199
200
 
200
201
  describe('error scenarios', () => {
201
202
  it('re-throws synchronous errors from provider', async () => {
202
- const oauthProvider = makeRejectingProvider(
203
- new Error('network failure')
204
- );
203
+ const oauthProvider = makeRejectingProvider(new Error('network failure'));
205
204
 
206
205
  await expect(
207
- grantAccessAlgorithm(userId, provider, accessToken, refreshToken, oauthProvider)
206
+ grantAccessAlgorithm(
207
+ userId,
208
+ provider,
209
+ accessToken,
210
+ refreshToken,
211
+ oauthProvider
212
+ )
208
213
  ).rejects.toThrow('network failure');
209
214
  });
210
215
 
@@ -220,7 +225,13 @@ describe('grantAccessAlgorithm', () => {
220
225
  const oauthProvider = makeRejectingProvider(originalError);
221
226
 
222
227
  await expect(
223
- grantAccessAlgorithm(userId, provider, accessToken, refreshToken, oauthProvider)
228
+ grantAccessAlgorithm(
229
+ userId,
230
+ provider,
231
+ accessToken,
232
+ refreshToken,
233
+ oauthProvider
234
+ )
224
235
  ).rejects.toThrow('token expired');
225
236
  });
226
237
 
@@ -228,7 +239,13 @@ describe('grantAccessAlgorithm', () => {
228
239
  const oauthProvider = makeRejectingProvider(new Error('boom'));
229
240
 
230
241
  await expect(
231
- grantAccessAlgorithm(userId, provider, accessToken, refreshToken, oauthProvider)
242
+ grantAccessAlgorithm(
243
+ userId,
244
+ provider,
245
+ accessToken,
246
+ refreshToken,
247
+ oauthProvider
248
+ )
232
249
  ).rejects.toThrow('boom');
233
250
 
234
251
  expect(oauthProvider.grantAccess).toHaveBeenCalledOnce();
@@ -49,7 +49,7 @@ export function getTierFromPriceId(priceId: string): string {
49
49
  * @since 0.0.1
50
50
  * @author AMBROISE PARK Consulting
51
51
  */
52
- export async function updateUserSubscription(
52
+ export async function updateFirebaseUserSubscription(
53
53
  firebaseUid: string,
54
54
  subscription: any
55
55
  ): Promise<void> {
@@ -171,7 +171,7 @@ export async function cancelUserSubscription(
171
171
  .doc(firebaseUid)
172
172
  .update({
173
173
  ...subscriptionClaims,
174
- updatedAt: Date.now(),
174
+ updatedAt: new Date().toISOString(),
175
175
  });
176
176
 
177
177
  console.log(`Subscription canceled for user ${firebaseUid}:`, {
@@ -74,7 +74,10 @@ export async function assertAdmin(uid: string): Promise<string> {
74
74
  } catch (error) {
75
75
  // C4: Re-throw permission-denied as-is so callers can distinguish it from
76
76
  // infrastructure failures. Only wrap genuine unexpected errors.
77
- if (error instanceof Error && error.message === 'Admin privileges required') {
77
+ if (
78
+ error instanceof Error &&
79
+ error.message === 'Admin privileges required'
80
+ ) {
78
81
  throw error;
79
82
  }
80
83
  throw new Error('Failed to verify admin status');
@@ -161,7 +164,8 @@ async function verifySupabaseToken(token: string): Promise<{ uid: string }> {
161
164
  const { createClient } = await import('@supabase/supabase-js');
162
165
 
163
166
  const supabaseUrl = process.env.SUPABASE_URL;
164
- const secretKey = process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
167
+ const secretKey =
168
+ process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
165
169
 
166
170
  if (!supabaseUrl || !secretKey) {
167
171
  throw new Error(
@@ -173,7 +177,10 @@ async function verifySupabaseToken(token: string): Promise<{ uid: string }> {
173
177
  auth: { autoRefreshToken: false, persistSession: false },
174
178
  });
175
179
 
176
- const { data: { user }, error } = await supabaseAdmin.auth.getUser(token);
180
+ const {
181
+ data: { user },
182
+ error,
183
+ } = await supabaseAdmin.auth.getUser(token);
177
184
 
178
185
  if (error || !user) {
179
186
  throw new Error('Invalid or expired token');
@@ -12,7 +12,10 @@
12
12
  import { logger } from 'firebase-functions/v2';
13
13
 
14
14
  import { getFirebaseAdminFirestore } from '@donotdev/firebase/server';
15
- import type { ServerRateLimitConfig as RateLimitConfig, ServerRateLimitResult as RateLimitResult } from '@donotdev/core';
15
+ import type {
16
+ ServerRateLimitConfig as RateLimitConfig,
17
+ ServerRateLimitResult as RateLimitResult,
18
+ } from '@donotdev/core';
16
19
 
17
20
  export type { RateLimitConfig, RateLimitResult };
18
21
 
@@ -204,7 +207,10 @@ export async function checkRateLimitWithFirestore(
204
207
  }
205
208
 
206
209
  // Increment attempts
207
- tx.update(rateLimitRef, { attempts: data.attempts + 1, lastUpdated: now });
210
+ tx.update(rateLimitRef, {
211
+ attempts: data.attempts + 1,
212
+ lastUpdated: now,
213
+ });
208
214
  result = {
209
215
  allowed: true,
210
216
  remaining: config.maxAttempts - (data.attempts + 1),
@@ -20,6 +20,11 @@ import {
20
20
  initFirebaseAdmin,
21
21
  } from '@donotdev/firebase/server';
22
22
 
23
+ import {
24
+ assertAuthenticated as internalAssertAuthenticated,
25
+ assertAdmin as internalAssertAdmin,
26
+ } from './utils/internal/auth.js';
27
+
23
28
  // Re-export DoNotDevError for external use
24
29
  export { DoNotDevError };
25
30
 
@@ -103,20 +108,24 @@ export const stripe = new Proxy({} as Stripe, {
103
108
 
104
109
  /**
105
110
  * 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.
111
+ *
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.
107
115
  *
108
116
  * @param auth - Firebase callable request auth context (object with `.uid`)
109
117
  * @returns The authenticated user's uid
110
118
  *
111
- * @version 0.0.2
119
+ * @version 0.0.3
112
120
  * @since 0.0.1
113
121
  * @author AMBROISE PARK Consulting
114
122
  */
115
123
  export function assertAuthenticated(auth: any): string {
116
- if (!auth || !auth.uid) {
124
+ const uid = auth?.uid;
125
+ if (!uid) {
117
126
  throw new Error('User must be authenticated');
118
127
  }
119
- return auth.uid;
128
+ return internalAssertAuthenticated(uid);
120
129
  }
121
130
 
122
131
  /**
@@ -317,35 +326,15 @@ export async function handleSubscriptionCancellation(
317
326
  * Asserts that a user has admin privileges
318
327
  * Uses role hierarchy: super > admin > user > guest
319
328
  *
320
- * @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
321
333
  * @since 0.0.1
322
334
  * @author AMBROISE PARK Consulting
323
335
  */
324
336
  export async function assertAdmin(uid: string): Promise<string> {
325
- if (!uid) {
326
- throw new DoNotDevError('Authentication required', 'unauthenticated');
327
- }
328
-
329
- try {
330
- const user = await getFirebaseAdminAuth().getUser(uid);
331
- const claims = user.customClaims || {};
332
-
333
- // Check role claim first (standard pattern)
334
- const role = claims.role;
335
- if (role === 'admin' || role === 'super') {
336
- return uid;
337
- }
338
-
339
- // Fallback: check legacy boolean flags
340
- if (claims.isAdmin === true || claims.isSuper === true) {
341
- return uid;
342
- }
343
-
344
- throw new DoNotDevError('Admin privileges required', 'permission-denied');
345
- } catch (error) {
346
- if (error instanceof DoNotDevError) throw error;
347
- throw new DoNotDevError('Failed to verify admin status', 'internal');
348
- }
337
+ return internalAssertAdmin(uid);
349
338
  }
350
339
 
351
340
  /**
@@ -450,7 +439,11 @@ export async function findReferences(
450
439
  }>;
451
440
  }
452
441
  ): Promise<Array<{ collection: string; field: string; count: number }>> {
453
- const references: Array<{ collection: string; field: string; count: number }> = [];
442
+ const references: Array<{
443
+ collection: string;
444
+ field: string;
445
+ count: number;
446
+ }> = [];
454
447
 
455
448
  if (!referenceMetadata?.incoming?.length) {
456
449
  return references;
@@ -17,9 +17,7 @@ import { createSupabaseHandler } from '../baseFunction.js';
17
17
  // Schema
18
18
  // =============================================================================
19
19
 
20
- const deleteAccountSchema = v.object({
21
- userId: v.pipe(v.string(), v.minLength(1, 'User ID is required')),
22
- });
20
+ const deleteAccountSchema = v.object({});
23
21
 
24
22
  // =============================================================================
25
23
  // Handler
@@ -44,16 +42,11 @@ export function createDeleteAccount() {
44
42
  return createSupabaseHandler(
45
43
  'delete-account',
46
44
  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);
45
+ async (_data, ctx) => {
46
+ const { error } = await ctx.supabaseAdmin.auth.admin.deleteUser(ctx.uid);
54
47
  if (error) throw error;
55
48
 
56
49
  return { success: true };
57
- },
50
+ }
58
51
  );
59
52
  }
@@ -43,14 +43,16 @@ export function createGetCustomClaims() {
43
43
  'get-custom-claims',
44
44
  getCustomClaimsSchema,
45
45
  async (_data, ctx) => {
46
- const { data: { user }, error } =
47
- await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
46
+ const {
47
+ data: { user },
48
+ error,
49
+ } = await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
48
50
  if (error || !user) {
49
51
  throw new Error('User not found');
50
52
  }
51
53
 
52
54
  return { customClaims: user.app_metadata ?? {} };
53
55
  },
54
- 'user',
56
+ 'user'
55
57
  );
56
58
  }
@@ -45,8 +45,10 @@ export function createGetUserAuthStatus() {
45
45
  'get-user-auth-status',
46
46
  getUserAuthStatusSchema,
47
47
  async (_data, ctx) => {
48
- const { data: { user }, error } =
49
- await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
48
+ const {
49
+ data: { user },
50
+ error,
51
+ } = await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
50
52
  if (error || !user) {
51
53
  throw new Error('User not found');
52
54
  }
@@ -59,6 +61,6 @@ export function createGetUserAuthStatus() {
59
61
  disabled: user.banned_until != null,
60
62
  };
61
63
  },
62
- 'user',
64
+ 'user'
63
65
  );
64
66
  }
@@ -20,7 +20,7 @@ import { createSupabaseHandler } from '../baseFunction.js';
20
20
  const removeCustomClaimsSchema = v.object({
21
21
  claimsToRemove: v.pipe(
22
22
  v.array(v.string()),
23
- v.minLength(1, 'At least one claim key is required'),
23
+ v.minLength(1, 'At least one claim key is required')
24
24
  ),
25
25
  });
26
26
 
@@ -49,14 +49,19 @@ export function createRemoveCustomClaims() {
49
49
  removeCustomClaimsSchema,
50
50
  async (data, ctx) => {
51
51
  // Get current app_metadata
52
- const { data: { user }, error: getUserError } =
53
- await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
52
+ const {
53
+ data: { user },
54
+ error: getUserError,
55
+ } = await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
54
56
  if (getUserError || !user) {
55
57
  throw new Error('User not found');
56
58
  }
57
59
 
58
60
  // Remove specified keys
59
- const existingClaims = { ...(user.app_metadata ?? {}) } as Record<string, unknown>;
61
+ const existingClaims = { ...(user.app_metadata ?? {}) } as Record<
62
+ string,
63
+ unknown
64
+ >;
60
65
  for (const key of data.claimsToRemove) {
61
66
  delete existingClaims[key];
62
67
  }
@@ -70,6 +75,6 @@ export function createRemoveCustomClaims() {
70
75
 
71
76
  return { success: true, customClaims: existingClaims };
72
77
  },
73
- 'user',
78
+ 'user'
74
79
  );
75
80
  }
@@ -49,14 +49,19 @@ export function createSetCustomClaims() {
49
49
  setCustomClaimsSchema,
50
50
  async (data, ctx) => {
51
51
  // Get current app_metadata
52
- const { data: { user }, error: getUserError } =
53
- await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
52
+ const {
53
+ data: { user },
54
+ error: getUserError,
55
+ } = await ctx.supabaseAdmin.auth.admin.getUserById(ctx.uid);
54
56
  if (getUserError || !user) {
55
57
  throw new Error('User not found');
56
58
  }
57
59
 
58
60
  // Merge new claims with existing app_metadata
59
- const existingClaims = (user.app_metadata ?? {}) as Record<string, unknown>;
61
+ const existingClaims = (user.app_metadata ?? {}) as Record<
62
+ string,
63
+ unknown
64
+ >;
60
65
  const updatedClaims = { ...existingClaims, ...data.customClaims };
61
66
 
62
67
  // Update app_metadata
@@ -68,6 +73,6 @@ export function createSetCustomClaims() {
68
73
 
69
74
  return { success: true, customClaims: updatedClaims };
70
75
  },
71
- 'user',
76
+ 'user'
72
77
  );
73
78
  }
@@ -103,7 +103,8 @@ export function createSupabaseHandler<TReq, TRes>(
103
103
  headers: {
104
104
  'Access-Control-Allow-Origin': allowedOrigin,
105
105
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
106
- 'Access-Control-Allow-Headers': 'authorization, content-type, x-client-info, apikey',
106
+ 'Access-Control-Allow-Headers':
107
+ 'authorization, content-type, x-client-info, apikey',
107
108
  },
108
109
  });
109
110
  }
@@ -116,19 +117,32 @@ export function createSupabaseHandler<TReq, TRes>(
116
117
  // Extract and verify auth token
117
118
  const authHeader = req.headers.get('authorization');
118
119
  if (!authHeader?.startsWith('Bearer ')) {
119
- return respond({ error: 'Missing or invalid authorization header' }, 401);
120
+ return respond(
121
+ { error: 'Missing or invalid authorization header' },
122
+ 401
123
+ );
120
124
  }
121
125
  const token = authHeader.slice(7);
122
126
 
123
127
  // Create admin client
124
128
  const supabaseUrl = getEnvOrThrow('SUPABASE_URL');
125
- const secretKey = getEnvOrThrow('SUPABASE_SECRET_KEY') || getEnvOrThrow('SUPABASE_SERVICE_ROLE_KEY'); // New: sb_secret_..., Legacy: service_role
129
+ // Try new env var first, fall back to legacy name
130
+ const secretKey =
131
+ getEnv('SUPABASE_SECRET_KEY') || getEnv('SUPABASE_SERVICE_ROLE_KEY');
132
+ if (!secretKey) {
133
+ throw new Error(
134
+ 'Missing environment variable: SUPABASE_SECRET_KEY or SUPABASE_SERVICE_ROLE_KEY'
135
+ );
136
+ }
126
137
  const supabaseAdmin = createClient(supabaseUrl, secretKey, {
127
138
  auth: { autoRefreshToken: false, persistSession: false },
128
139
  });
129
140
 
130
141
  // Verify JWT and extract user
131
- const { data: { user }, error: authError } = await supabaseAdmin.auth.getUser(token);
142
+ const {
143
+ data: { user },
144
+ error: authError,
145
+ } = await supabaseAdmin.auth.getUser(token);
132
146
  if (authError || !user) {
133
147
  return respond({ error: 'Invalid or expired token' }, 401);
134
148
  }
@@ -144,7 +158,9 @@ export function createSupabaseHandler<TReq, TRes>(
144
158
  // Non-guest access: verify user has required role level
145
159
  if (!hasRoleAccess(userRole, requiredRole)) {
146
160
  return respond(
147
- { error: `Access denied. Required role: ${requiredRole}, your role: ${userRole}` },
161
+ {
162
+ error: `Access denied. Required role: ${requiredRole}, your role: ${userRole}`,
163
+ },
148
164
  403
149
165
  );
150
166
  }
@@ -160,8 +176,9 @@ export function createSupabaseHandler<TReq, TRes>(
160
176
  : `uid_${user.id}`;
161
177
  const rateLimitKey = `${operationName}_${rateLimitIdentifier}`;
162
178
  const rateLimitConfig: RateLimitConfig =
163
- (DEFAULT_RATE_LIMITS as Record<string, RateLimitConfig>)[operationName] ||
164
- DEFAULT_RATE_LIMITS.api;
179
+ (DEFAULT_RATE_LIMITS as Record<string, RateLimitConfig>)[
180
+ operationName
181
+ ] || DEFAULT_RATE_LIMITS.api;
165
182
 
166
183
  const rateLimitResult = await checkRateLimitWithPostgres(
167
184
  supabaseAdmin,
@@ -206,7 +223,10 @@ export function createSupabaseHandler<TReq, TRes>(
206
223
  supabaseAdmin,
207
224
  });
208
225
  } catch (handlerError) {
209
- error = handlerError instanceof Error ? handlerError : new Error(String(handlerError));
226
+ error =
227
+ handlerError instanceof Error
228
+ ? handlerError
229
+ : new Error(String(handlerError));
210
230
  throw handlerError;
211
231
  } finally {
212
232
  // Record metrics if enabled
@@ -229,7 +249,8 @@ export function createSupabaseHandler<TReq, TRes>(
229
249
  return respond(result, 200);
230
250
  } catch (error) {
231
251
  console.error(`[${operationName}] Error:`, error);
232
- const message = error instanceof Error ? error.message : 'Internal server error';
252
+ const message =
253
+ error instanceof Error ? error.message : 'Internal server error';
233
254
  const status = getErrorStatus(message);
234
255
  // allowedOrigin may not be set if error occurred before that line; fall back to '*'
235
256
  const origin = getEnv('ALLOWED_ORIGIN', '*');
@@ -242,7 +263,11 @@ export function createSupabaseHandler<TReq, TRes>(
242
263
  // Helpers
243
264
  // =============================================================================
244
265
 
245
- function jsonResponse(data: unknown, status: number, allowedOrigin = '*'): Response {
266
+ function jsonResponse(
267
+ data: unknown,
268
+ status: number,
269
+ allowedOrigin = '*'
270
+ ): Response {
246
271
  return new Response(JSON.stringify(data), {
247
272
  status,
248
273
  headers: {
@@ -253,13 +278,19 @@ function jsonResponse(data: unknown, status: number, allowedOrigin = '*'): Respo
253
278
  }
254
279
 
255
280
  function getEnvOrThrow(key: string): string {
256
- const value = (typeof Deno !== 'undefined' ? Deno.env.get(key) : process.env[key]) as string | undefined;
281
+ const value = (
282
+ typeof Deno !== 'undefined' ? Deno.env.get(key) : process.env[key]
283
+ ) as string | undefined;
257
284
  if (!value) throw new Error(`Missing environment variable: ${key}`);
258
285
  return value;
259
286
  }
260
287
 
261
288
  function getEnv(key: string, defaultValue: string = ''): string {
262
- return (typeof Deno !== 'undefined' ? Deno.env.get(key) : process.env[key]) as string | undefined || defaultValue;
289
+ return (
290
+ ((typeof Deno !== 'undefined' ? Deno.env.get(key) : process.env[key]) as
291
+ | string
292
+ | undefined) || defaultValue
293
+ );
263
294
  }
264
295
 
265
296
  function getClientIp(req: Request): string {
@@ -269,7 +300,10 @@ function getClientIp(req: Request): string {
269
300
  // W19: Take the rightmost (last) IP — the leftmost entry is client-supplied
270
301
  // and trivially spoofable. The last entry is appended by the nearest trusted
271
302
  // reverse proxy and is the most reliable.
272
- const ips = forwardedFor.split(',').map((ip) => ip.trim()).filter(Boolean);
303
+ const ips = forwardedFor
304
+ .split(',')
305
+ .map((ip) => ip.trim())
306
+ .filter(Boolean);
273
307
  const lastIp = ips[ips.length - 1];
274
308
  if (lastIp) return lastIp;
275
309
  }
@@ -283,20 +317,45 @@ function getClientIp(req: Request): string {
283
317
 
284
318
  function getErrorCode(message: string): string {
285
319
  if (message.includes('Rate limit')) return 'rate-limit-exceeded';
286
- if (message.includes('not found') || message.includes('No active')) return 'not-found';
287
- if (message.includes('permission') || message.includes('denied') || message.includes('Forbidden')) return 'permission-denied';
288
- if (message.includes('mismatch') || message.includes('Invalid') || message.includes('Missing')) return 'invalid-argument';
289
- if (message.includes('already-exists') || message.includes('Duplicate')) return 'already-exists';
320
+ if (message.includes('not found') || message.includes('No active'))
321
+ return 'not-found';
322
+ if (
323
+ message.includes('permission') ||
324
+ message.includes('denied') ||
325
+ message.includes('Forbidden')
326
+ )
327
+ return 'permission-denied';
328
+ if (
329
+ message.includes('mismatch') ||
330
+ message.includes('Invalid') ||
331
+ message.includes('Missing')
332
+ )
333
+ return 'invalid-argument';
334
+ if (message.includes('already-exists') || message.includes('Duplicate'))
335
+ return 'already-exists';
290
336
  return 'internal';
291
337
  }
292
338
 
293
339
  function getErrorStatus(message: string): number {
294
340
  if (message.includes('Rate limit')) return 429;
295
- if (message.includes('not found') || message.includes('No active')) return 404;
296
- if (message.includes('permission') || message.includes('denied') || message.includes('Forbidden')) return 403;
297
- if (message.includes('mismatch') || message.includes('Invalid') || message.includes('Missing')) return 400;
341
+ if (message.includes('not found') || message.includes('No active'))
342
+ return 404;
343
+ if (
344
+ message.includes('permission') ||
345
+ message.includes('denied') ||
346
+ message.includes('Forbidden')
347
+ )
348
+ return 403;
349
+ if (
350
+ message.includes('mismatch') ||
351
+ message.includes('Invalid') ||
352
+ message.includes('Missing')
353
+ )
354
+ return 400;
298
355
  return 500;
299
356
  }
300
357
 
301
358
  // Deno global type declaration for env access
302
- declare const Deno: { env: { get(key: string): string | undefined } } | undefined;
359
+ declare const Deno:
360
+ | { env: { get(key: string): string | undefined } }
361
+ | undefined;
@@ -44,14 +44,20 @@ export function createCancelSubscription() {
44
44
  initStripe(getStripeKey());
45
45
  const authProvider = createSupabaseAuthProvider(ctx.supabaseAdmin);
46
46
  return cancelUserSubscription(data.userId, authProvider);
47
- },
47
+ }
48
48
  );
49
49
  }
50
50
 
51
51
  function getStripeKey(): string {
52
- const key = (typeof Deno !== 'undefined' ? Deno.env.get('STRIPE_SECRET_KEY') : process.env.STRIPE_SECRET_KEY) as string | undefined;
52
+ const key = (
53
+ typeof Deno !== 'undefined'
54
+ ? Deno.env.get('STRIPE_SECRET_KEY')
55
+ : process.env.STRIPE_SECRET_KEY
56
+ ) as string | undefined;
53
57
  if (!key) throw new Error('Missing STRIPE_SECRET_KEY environment variable');
54
58
  return key;
55
59
  }
56
60
 
57
- declare const Deno: { env: { get(key: string): string | undefined } } | undefined;
61
+ declare const Deno:
62
+ | { env: { get(key: string): string | undefined } }
63
+ | undefined;