@donotdev/functions 0.0.12 → 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 (50) hide show
  1. package/package.json +4 -4
  2. package/src/firebase/auth/setCustomClaims.ts +19 -5
  3. package/src/firebase/baseFunction.ts +11 -3
  4. package/src/firebase/billing/changePlan.ts +5 -1
  5. package/src/firebase/billing/createCustomerPortal.ts +6 -2
  6. package/src/firebase/billing/webhookHandler.ts +4 -1
  7. package/src/firebase/crud/aggregate.ts +5 -1
  8. package/src/firebase/crud/create.ts +17 -4
  9. package/src/firebase/crud/list.ts +9 -4
  10. package/src/firebase/crud/update.ts +17 -4
  11. package/src/firebase/oauth/exchangeToken.ts +17 -4
  12. package/src/shared/__tests__/validation.test.ts +5 -3
  13. package/src/shared/billing/__tests__/createCheckout.test.ts +123 -22
  14. package/src/shared/billing/__tests__/webhookHandler.test.ts +121 -30
  15. package/src/shared/firebase.ts +1 -1
  16. package/src/shared/logger.ts +7 -1
  17. package/src/shared/oauth/__tests__/exchangeToken.test.ts +6 -8
  18. package/src/shared/oauth/__tests__/grantAccess.test.ts +31 -14
  19. package/src/shared/utils/internal/auth.ts +10 -3
  20. package/src/shared/utils/internal/rateLimiter.ts +8 -2
  21. package/src/shared/utils.ts +5 -1
  22. package/src/supabase/auth/deleteAccount.ts +1 -1
  23. package/src/supabase/auth/getCustomClaims.ts +5 -3
  24. package/src/supabase/auth/getUserAuthStatus.ts +5 -3
  25. package/src/supabase/auth/removeCustomClaims.ts +10 -5
  26. package/src/supabase/auth/setCustomClaims.ts +9 -4
  27. package/src/supabase/baseFunction.ts +77 -22
  28. package/src/supabase/billing/cancelSubscription.ts +9 -3
  29. package/src/supabase/billing/changePlan.ts +20 -5
  30. package/src/supabase/billing/createCheckoutSession.ts +20 -5
  31. package/src/supabase/billing/createCustomerPortal.ts +14 -4
  32. package/src/supabase/billing/refreshSubscriptionStatus.ts +29 -9
  33. package/src/supabase/crud/aggregate.ts +14 -4
  34. package/src/supabase/crud/create.ts +30 -11
  35. package/src/supabase/crud/delete.ts +11 -3
  36. package/src/supabase/crud/get.ts +25 -3
  37. package/src/supabase/crud/list.ts +76 -22
  38. package/src/supabase/crud/update.ts +32 -10
  39. package/src/supabase/helpers/authProvider.ts +5 -2
  40. package/src/supabase/index.ts +1 -4
  41. package/src/supabase/registerCrudFunctions.ts +11 -9
  42. package/src/supabase/utils/idempotency.ts +13 -15
  43. package/src/supabase/utils/monitoring.ts +5 -1
  44. package/src/supabase/utils/rateLimiter.ts +13 -3
  45. package/src/vercel/api/billing/webhook-handler.ts +6 -2
  46. package/src/vercel/api/crud/create.ts +7 -2
  47. package/src/vercel/api/crud/delete.ts +3 -1
  48. package/src/vercel/api/crud/get.ts +3 -1
  49. package/src/vercel/api/crud/list.ts +3 -1
  50. package/src/vercel/api/crud/update.ts +7 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@donotdev/functions",
3
- "version": "0.0.12",
3
+ "version": "0.0.13",
4
4
  "private": false,
5
5
  "description": "Backend functions for DoNotDev Framework - Firebase, Vercel, and platform-agnostic implementations for auth, billing, CRUD, and OAuth",
6
6
  "main": "./lib/firebase/index.js",
@@ -47,9 +47,9 @@
47
47
  "serve": "firebase emulators:start --only functions"
48
48
  },
49
49
  "dependencies": {
50
- "@donotdev/core": "^0.0.25",
51
- "@donotdev/firebase": "^0.0.12",
52
- "@donotdev/supabase": "^0.0.2"
50
+ "@donotdev/core": "^0.0.26",
51
+ "@donotdev/firebase": "^0.0.13",
52
+ "@donotdev/supabase": "^0.0.3"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@sentry/node": "^10.39.0",
@@ -60,11 +60,19 @@ async function setCustomClaimsLogic(
60
60
 
61
61
  // W17: Validate idempotency key to prevent oversized or malformed inputs.
62
62
  if (idempotencyKey !== undefined) {
63
- if (typeof idempotencyKey !== 'string' || idempotencyKey.length === 0 || idempotencyKey.length > 256) {
64
- throw new Error('idempotencyKey must be a non-empty string of at most 256 characters');
63
+ if (
64
+ typeof idempotencyKey !== 'string' ||
65
+ idempotencyKey.length === 0 ||
66
+ idempotencyKey.length > 256
67
+ ) {
68
+ throw new Error(
69
+ 'idempotencyKey must be a non-empty string of at most 256 characters'
70
+ );
65
71
  }
66
72
  if (!/^[\w\-.:@]+$/.test(idempotencyKey)) {
67
- throw new Error('idempotencyKey contains invalid characters (allowed: alphanumeric, -, _, ., :, @)');
73
+ throw new Error(
74
+ 'idempotencyKey contains invalid characters (allowed: alphanumeric, -, _, ., :, @)'
75
+ );
68
76
  }
69
77
  }
70
78
 
@@ -85,11 +93,17 @@ async function setCustomClaimsLogic(
85
93
  alreadyProcessed = true;
86
94
  return;
87
95
  }
88
- tx.set(idempotencyRef, { processing: true, reservedAt: new Date().toISOString() });
96
+ tx.set(idempotencyRef, {
97
+ processing: true,
98
+ reservedAt: new Date().toISOString(),
99
+ });
89
100
  });
90
101
 
91
102
  if (alreadyProcessed) {
92
- return existingResult as { success: boolean; customClaims: Record<string, any> };
103
+ return existingResult as {
104
+ success: boolean;
105
+ customClaims: Record<string, any>;
106
+ };
93
107
  }
94
108
  }
95
109
 
@@ -21,7 +21,10 @@ import { FUNCTION_CONFIG } from './config/constants.js';
21
21
  import { handleError } from '../shared/errorHandling.js';
22
22
  import { assertAuthenticated, getUserRole } from '../shared/utils.js';
23
23
 
24
- import type { CallableRequest, CallableOptions } from 'firebase-functions/v2/https';
24
+ import type {
25
+ CallableRequest,
26
+ CallableOptions,
27
+ } from 'firebase-functions/v2/https';
25
28
 
26
29
  // Optional monitoring imports - only used when enabled
27
30
  // Lazy loaded to avoid unnecessary Firestore operations
@@ -76,9 +79,14 @@ async function loadMonitoring() {
76
79
  function getClientIp(request: CallableRequest<unknown>): string {
77
80
  const forwardedFor = request.rawRequest.headers['x-forwarded-for'];
78
81
  if (forwardedFor) {
79
- const raw = Array.isArray(forwardedFor) ? forwardedFor.join(',') : forwardedFor;
82
+ const raw = Array.isArray(forwardedFor)
83
+ ? forwardedFor.join(',')
84
+ : forwardedFor;
80
85
  // Split and take the RIGHTMOST entry (last untrusted / first-to-be-trusted)
81
- const ips = raw.split(',').map((s) => s.trim()).filter(Boolean);
86
+ const ips = raw
87
+ .split(',')
88
+ .map((s) => s.trim())
89
+ .filter(Boolean);
82
90
  if (ips.length > 0) {
83
91
  return ips[ips.length - 1]!;
84
92
  }
@@ -17,7 +17,11 @@ import { getFirebaseAdminAuth } from '@donotdev/firebase/server';
17
17
 
18
18
  import { updateUserSubscription } from '../../shared/billing/helpers/updateUserSubscription.js';
19
19
  import { handleError } from '../../shared/errorHandling.js';
20
- import { stripe, validateStripeEnvironment, initStripe } from '../../shared/utils.js';
20
+ import {
21
+ stripe,
22
+ validateStripeEnvironment,
23
+ initStripe,
24
+ } from '../../shared/utils.js';
21
25
  import { createBaseFunction } from '../baseFunction.js';
22
26
  import { STRIPE_CONFIG } from '../config/constants.js';
23
27
  import { stripeSecretKey } from '../config/secrets.js';
@@ -70,10 +70,14 @@ async function createCustomerPortalLogic(
70
70
  // Use caller-supplied returnUrl or derive from FRONTEND_URL env var.
71
71
  const resolvedReturnUrl =
72
72
  returnUrl ??
73
- (process.env.FRONTEND_URL ? `${process.env.FRONTEND_URL}/dashboard` : undefined);
73
+ (process.env.FRONTEND_URL
74
+ ? `${process.env.FRONTEND_URL}/dashboard`
75
+ : undefined);
74
76
 
75
77
  if (!resolvedReturnUrl) {
76
- throw handleError(new Error('returnUrl is required (or set FRONTEND_URL env var)'));
78
+ throw handleError(
79
+ new Error('returnUrl is required (or set FRONTEND_URL env var)')
80
+ );
77
81
  }
78
82
 
79
83
  // Create portal session
@@ -102,7 +102,10 @@ export function createStripeWebhook(
102
102
  async getUser(userId: string) {
103
103
  return getFirebaseAdminAuth().getUser(userId);
104
104
  },
105
- async setCustomUserClaims(userId: string, claims: Record<string, unknown>) {
105
+ async setCustomUserClaims(
106
+ userId: string,
107
+ claims: Record<string, unknown>
108
+ ) {
106
109
  await getFirebaseAdminAuth().setCustomUserClaims(userId, claims);
107
110
  },
108
111
  };
@@ -220,7 +220,11 @@ function aggregateEntitiesLogicFactory(
220
220
  ) {
221
221
  return async function aggregateEntitiesLogic(
222
222
  data: AggregateRequest,
223
- context: { uid: string; userRole: UserRole; request: CallableRequest<AggregateRequest> }
223
+ context: {
224
+ uid: string;
225
+ userRole: UserRole;
226
+ request: CallableRequest<AggregateRequest>;
227
+ }
224
228
  ) {
225
229
  const db = getFirebaseAdminFirestore();
226
230
  const { userRole } = context;
@@ -160,11 +160,21 @@ function createEntityLogicFactory(
160
160
 
161
161
  // W17: Validate idempotency key length and content.
162
162
  if (idempotencyKey !== undefined) {
163
- if (typeof idempotencyKey !== 'string' || idempotencyKey.length === 0 || idempotencyKey.length > 256) {
164
- throw new DoNotDevError('idempotencyKey must be a non-empty string of at most 256 characters', 'invalid-argument');
163
+ if (
164
+ typeof idempotencyKey !== 'string' ||
165
+ idempotencyKey.length === 0 ||
166
+ idempotencyKey.length > 256
167
+ ) {
168
+ throw new DoNotDevError(
169
+ 'idempotencyKey must be a non-empty string of at most 256 characters',
170
+ 'invalid-argument'
171
+ );
165
172
  }
166
173
  if (!/^[\w\-.:@]+$/.test(idempotencyKey)) {
167
- throw new DoNotDevError('idempotencyKey contains invalid characters', 'invalid-argument');
174
+ throw new DoNotDevError(
175
+ 'idempotencyKey contains invalid characters',
176
+ 'invalid-argument'
177
+ );
168
178
  }
169
179
  }
170
180
 
@@ -187,7 +197,10 @@ function createEntityLogicFactory(
187
197
  alreadyProcessed = true;
188
198
  return;
189
199
  }
190
- tx.set(idempotencyRef, { processing: true, reservedAt: new Date().toISOString() });
200
+ tx.set(idempotencyRef, {
201
+ processing: true,
202
+ reservedAt: new Date().toISOString(),
203
+ });
191
204
  });
192
205
 
193
206
  if (alreadyProcessed) {
@@ -137,7 +137,11 @@ function listEntitiesLogicFactory(
137
137
  // Apply where clauses for filtering — validate field names against entity schema
138
138
  for (const [field, operator, value] of where) {
139
139
  if (listFields && listFields.length > 0) {
140
- if (!listFields.includes(field) && field !== 'status' && field !== 'id') {
140
+ if (
141
+ !listFields.includes(field) &&
142
+ field !== 'status' &&
143
+ field !== 'id'
144
+ ) {
141
145
  throw new DoNotDevError(
142
146
  `Where field '${field}' is not allowed`,
143
147
  'invalid-argument'
@@ -173,9 +177,10 @@ function listEntitiesLogicFactory(
173
177
 
174
178
  // W13: Cap at MAX_LIST_LIMIT to prevent unbounded reads (DoS via cost amplification).
175
179
  const MAX_LIST_LIMIT = 500;
176
- const effectiveLimit = limit !== undefined && limit > 0
177
- ? Math.min(limit, MAX_LIST_LIMIT)
178
- : MAX_LIST_LIMIT;
180
+ const effectiveLimit =
181
+ limit !== undefined && limit > 0
182
+ ? Math.min(limit, MAX_LIST_LIMIT)
183
+ : MAX_LIST_LIMIT;
179
184
  query = query.limit(effectiveLimit);
180
185
 
181
186
  // Execute the query
@@ -153,11 +153,21 @@ function updateEntityLogicFactory(
153
153
 
154
154
  // W17: Validate idempotency key length and content.
155
155
  if (idempotencyKey !== undefined) {
156
- if (typeof idempotencyKey !== 'string' || idempotencyKey.length === 0 || idempotencyKey.length > 256) {
157
- throw new DoNotDevError('idempotencyKey must be a non-empty string of at most 256 characters', 'invalid-argument');
156
+ if (
157
+ typeof idempotencyKey !== 'string' ||
158
+ idempotencyKey.length === 0 ||
159
+ idempotencyKey.length > 256
160
+ ) {
161
+ throw new DoNotDevError(
162
+ 'idempotencyKey must be a non-empty string of at most 256 characters',
163
+ 'invalid-argument'
164
+ );
158
165
  }
159
166
  if (!/^[\w\-.:@]+$/.test(idempotencyKey)) {
160
- throw new DoNotDevError('idempotencyKey contains invalid characters', 'invalid-argument');
167
+ throw new DoNotDevError(
168
+ 'idempotencyKey contains invalid characters',
169
+ 'invalid-argument'
170
+ );
161
171
  }
162
172
  }
163
173
 
@@ -178,7 +188,10 @@ function updateEntityLogicFactory(
178
188
  alreadyProcessed = true;
179
189
  return;
180
190
  }
181
- tx.set(idempotencyRef, { processing: true, reservedAt: new Date().toISOString() });
191
+ tx.set(idempotencyRef, {
192
+ processing: true,
193
+ reservedAt: new Date().toISOString(),
194
+ });
182
195
  });
183
196
 
184
197
  if (alreadyProcessed) {
@@ -39,11 +39,21 @@ export const exchangeToken = onCall<ExchangeTokenRequest>(async (request) => {
39
39
 
40
40
  // W17: Validate idempotency key length and content.
41
41
  if (idempotencyKey !== undefined) {
42
- if (typeof idempotencyKey !== 'string' || idempotencyKey.length === 0 || idempotencyKey.length > 256) {
43
- throw new HttpsError('invalid-argument', 'idempotencyKey must be a non-empty string of at most 256 characters');
42
+ if (
43
+ typeof idempotencyKey !== 'string' ||
44
+ idempotencyKey.length === 0 ||
45
+ idempotencyKey.length > 256
46
+ ) {
47
+ throw new HttpsError(
48
+ 'invalid-argument',
49
+ 'idempotencyKey must be a non-empty string of at most 256 characters'
50
+ );
44
51
  }
45
52
  if (!/^[\w\-.:@]+$/.test(idempotencyKey)) {
46
- throw new HttpsError('invalid-argument', 'idempotencyKey contains invalid characters');
53
+ throw new HttpsError(
54
+ 'invalid-argument',
55
+ 'idempotencyKey contains invalid characters'
56
+ );
47
57
  }
48
58
  }
49
59
 
@@ -67,7 +77,10 @@ export const exchangeToken = onCall<ExchangeTokenRequest>(async (request) => {
67
77
  return;
68
78
  }
69
79
  // Reserve the key before executing business logic
70
- tx.set(idempotencyRef, { processing: true, reservedAt: new Date().toISOString() });
80
+ tx.set(idempotencyRef, {
81
+ processing: true,
82
+ reservedAt: new Date().toISOString(),
83
+ });
71
84
  });
72
85
 
73
86
  if (alreadyProcessed) {
@@ -108,7 +108,9 @@ describe('validateUrl', () => {
108
108
  });
109
109
 
110
110
  it('includes custom name in error message', () => {
111
- expect(() => validateUrl('bad', 'Success URL')).toThrow('Invalid Success URL');
111
+ expect(() => validateUrl('bad', 'Success URL')).toThrow(
112
+ 'Invalid Success URL'
113
+ );
112
114
  });
113
115
  });
114
116
 
@@ -121,10 +123,10 @@ describe('validateMetadata', () => {
121
123
 
122
124
  it('rejects non-string values', () => {
123
125
  expect(() => validateMetadata({ key: 123 as any })).toThrow(
124
- "must be a string"
126
+ 'must be a string'
125
127
  );
126
128
  expect(() => validateMetadata({ key: true as any })).toThrow(
127
- "must be a string"
129
+ 'must be a string'
128
130
  );
129
131
  });
130
132
 
@@ -61,7 +61,9 @@ const BASE_REQUEST: CreateCheckoutSessionRequest = {
61
61
  // Mock factories
62
62
  // ---------------------------------------------------------------------------
63
63
 
64
- function makeStripeProvider(overrides?: Partial<StripeCheckoutProvider>): StripeCheckoutProvider {
64
+ function makeStripeProvider(
65
+ overrides?: Partial<StripeCheckoutProvider>
66
+ ): StripeCheckoutProvider {
65
67
  return {
66
68
  createCheckoutSession: vi.fn().mockResolvedValue({
67
69
  id: 'cs_test_session123',
@@ -71,7 +73,9 @@ function makeStripeProvider(overrides?: Partial<StripeCheckoutProvider>): Stripe
71
73
  };
72
74
  }
73
75
 
74
- function makeAuthProvider(overrides?: Partial<AuthCheckoutProvider>): AuthCheckoutProvider {
76
+ function makeAuthProvider(
77
+ overrides?: Partial<AuthCheckoutProvider>
78
+ ): AuthCheckoutProvider {
75
79
  return {
76
80
  getUser: vi.fn().mockResolvedValue({ customClaims: {} }),
77
81
  ...overrides,
@@ -112,14 +116,24 @@ describe('createCheckoutAlgorithm', () => {
112
116
  });
113
117
 
114
118
  it('verifies the user before creating the session', async () => {
115
- await createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG);
119
+ await createCheckoutAlgorithm(
120
+ BASE_REQUEST,
121
+ stripeProvider,
122
+ authProvider,
123
+ BILLING_CONFIG
124
+ );
116
125
 
117
126
  expect(authProvider.getUser).toHaveBeenCalledOnce();
118
127
  expect(authProvider.getUser).toHaveBeenCalledWith('user_001');
119
128
  });
120
129
 
121
130
  it('forwards priceId, mode, successUrl, cancelUrl to Stripe', async () => {
122
- await createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG);
131
+ await createCheckoutAlgorithm(
132
+ BASE_REQUEST,
133
+ stripeProvider,
134
+ authProvider,
135
+ BILLING_CONFIG
136
+ );
123
137
 
124
138
  expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
125
139
  expect.objectContaining({
@@ -132,7 +146,12 @@ describe('createCheckoutAlgorithm', () => {
132
146
  });
133
147
 
134
148
  it('ensures userId and billingConfigKey are set in session metadata', async () => {
135
- await createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG);
149
+ await createCheckoutAlgorithm(
150
+ BASE_REQUEST,
151
+ stripeProvider,
152
+ authProvider,
153
+ BILLING_CONFIG
154
+ );
136
155
 
137
156
  expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
138
157
  expect.objectContaining({
@@ -150,7 +169,12 @@ describe('createCheckoutAlgorithm', () => {
150
169
  customerEmail: 'user@example.com',
151
170
  };
152
171
 
153
- await createCheckoutAlgorithm(request, stripeProvider, authProvider, BILLING_CONFIG);
172
+ await createCheckoutAlgorithm(
173
+ request,
174
+ stripeProvider,
175
+ authProvider,
176
+ BILLING_CONFIG
177
+ );
154
178
 
155
179
  expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
156
180
  expect.objectContaining({ customerEmail: 'user@example.com' })
@@ -163,7 +187,12 @@ describe('createCheckoutAlgorithm', () => {
163
187
  allowPromotionCodes: undefined,
164
188
  };
165
189
 
166
- await createCheckoutAlgorithm(request, stripeProvider, authProvider, BILLING_CONFIG);
190
+ await createCheckoutAlgorithm(
191
+ request,
192
+ stripeProvider,
193
+ authProvider,
194
+ BILLING_CONFIG
195
+ );
167
196
 
168
197
  expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
169
198
  expect.objectContaining({ allowPromotionCodes: true })
@@ -176,7 +205,12 @@ describe('createCheckoutAlgorithm', () => {
176
205
  mode: undefined,
177
206
  };
178
207
 
179
- await createCheckoutAlgorithm(request, stripeProvider, authProvider, BILLING_CONFIG);
208
+ await createCheckoutAlgorithm(
209
+ request,
210
+ stripeProvider,
211
+ authProvider,
212
+ BILLING_CONFIG
213
+ );
180
214
 
181
215
  expect(stripeProvider.createCheckoutSession).toHaveBeenCalledWith(
182
216
  expect.objectContaining({ mode: 'payment' })
@@ -233,7 +267,12 @@ describe('createCheckoutAlgorithm', () => {
233
267
  };
234
268
 
235
269
  await expect(
236
- createCheckoutAlgorithm(request, stripeProvider, authProvider, BILLING_CONFIG)
270
+ createCheckoutAlgorithm(
271
+ request,
272
+ stripeProvider,
273
+ authProvider,
274
+ BILLING_CONFIG
275
+ )
237
276
  ).rejects.toThrow('Missing billingConfigKey in metadata');
238
277
  });
239
278
 
@@ -244,7 +283,12 @@ describe('createCheckoutAlgorithm', () => {
244
283
  };
245
284
 
246
285
  await expect(
247
- createCheckoutAlgorithm(request, stripeProvider, authProvider, BILLING_CONFIG)
286
+ createCheckoutAlgorithm(
287
+ request,
288
+ stripeProvider,
289
+ authProvider,
290
+ BILLING_CONFIG
291
+ )
248
292
  ).rejects.toThrow('Missing billingConfigKey in metadata');
249
293
  });
250
294
 
@@ -255,7 +299,12 @@ describe('createCheckoutAlgorithm', () => {
255
299
  };
256
300
 
257
301
  await expect(
258
- createCheckoutAlgorithm(request, stripeProvider, authProvider, BILLING_CONFIG)
302
+ createCheckoutAlgorithm(
303
+ request,
304
+ stripeProvider,
305
+ authProvider,
306
+ BILLING_CONFIG
307
+ )
259
308
  ).rejects.toThrow();
260
309
 
261
310
  expect(stripeProvider.createCheckoutSession).not.toHaveBeenCalled();
@@ -274,7 +323,12 @@ describe('createCheckoutAlgorithm', () => {
274
323
  };
275
324
 
276
325
  await expect(
277
- createCheckoutAlgorithm(request, stripeProvider, authProvider, BILLING_CONFIG)
326
+ createCheckoutAlgorithm(
327
+ request,
328
+ stripeProvider,
329
+ authProvider,
330
+ BILLING_CONFIG
331
+ )
278
332
  ).rejects.toThrow('Invalid billing config key: nonexistent_plan');
279
333
  });
280
334
 
@@ -286,7 +340,12 @@ describe('createCheckoutAlgorithm', () => {
286
340
  };
287
341
 
288
342
  await expect(
289
- createCheckoutAlgorithm(request, stripeProvider, authProvider, BILLING_CONFIG)
343
+ createCheckoutAlgorithm(
344
+ request,
345
+ stripeProvider,
346
+ authProvider,
347
+ BILLING_CONFIG
348
+ )
290
349
  ).rejects.toThrow('Price ID mismatch with configuration');
291
350
  });
292
351
 
@@ -298,7 +357,12 @@ describe('createCheckoutAlgorithm', () => {
298
357
  };
299
358
 
300
359
  await expect(
301
- createCheckoutAlgorithm(request, stripeProvider, authProvider, BILLING_CONFIG)
360
+ createCheckoutAlgorithm(
361
+ request,
362
+ stripeProvider,
363
+ authProvider,
364
+ BILLING_CONFIG
365
+ )
302
366
  ).rejects.toThrow();
303
367
 
304
368
  expect(stripeProvider.createCheckoutSession).not.toHaveBeenCalled();
@@ -311,7 +375,12 @@ describe('createCheckoutAlgorithm', () => {
311
375
 
312
376
  describe('auth validation', () => {
313
377
  it('calls getUser with the request userId', async () => {
314
- await createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG);
378
+ await createCheckoutAlgorithm(
379
+ BASE_REQUEST,
380
+ stripeProvider,
381
+ authProvider,
382
+ BILLING_CONFIG
383
+ );
315
384
 
316
385
  expect(authProvider.getUser).toHaveBeenCalledWith('user_001');
317
386
  });
@@ -322,7 +391,12 @@ describe('createCheckoutAlgorithm', () => {
322
391
  });
323
392
 
324
393
  await expect(
325
- createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG)
394
+ createCheckoutAlgorithm(
395
+ BASE_REQUEST,
396
+ stripeProvider,
397
+ authProvider,
398
+ BILLING_CONFIG
399
+ )
326
400
  ).rejects.toThrow('User not found');
327
401
  });
328
402
 
@@ -332,7 +406,12 @@ describe('createCheckoutAlgorithm', () => {
332
406
  });
333
407
 
334
408
  await expect(
335
- createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG)
409
+ createCheckoutAlgorithm(
410
+ BASE_REQUEST,
411
+ stripeProvider,
412
+ authProvider,
413
+ BILLING_CONFIG
414
+ )
336
415
  ).rejects.toThrow();
337
416
 
338
417
  expect(stripeProvider.createCheckoutSession).not.toHaveBeenCalled();
@@ -346,11 +425,18 @@ describe('createCheckoutAlgorithm', () => {
346
425
  describe('Stripe error handling', () => {
347
426
  it('propagates Stripe API errors', async () => {
348
427
  stripeProvider = makeStripeProvider({
349
- createCheckoutSession: vi.fn().mockRejectedValue(new Error('Stripe network error')),
428
+ createCheckoutSession: vi
429
+ .fn()
430
+ .mockRejectedValue(new Error('Stripe network error')),
350
431
  });
351
432
 
352
433
  await expect(
353
- createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG)
434
+ createCheckoutAlgorithm(
435
+ BASE_REQUEST,
436
+ stripeProvider,
437
+ authProvider,
438
+ BILLING_CONFIG
439
+ )
354
440
  ).rejects.toThrow('Stripe network error');
355
441
  });
356
442
 
@@ -362,7 +448,12 @@ describe('createCheckoutAlgorithm', () => {
362
448
  });
363
449
 
364
450
  await expect(
365
- createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG)
451
+ createCheckoutAlgorithm(
452
+ BASE_REQUEST,
453
+ stripeProvider,
454
+ authProvider,
455
+ BILLING_CONFIG
456
+ )
366
457
  ).rejects.toThrow('Your card was declined');
367
458
  });
368
459
 
@@ -374,7 +465,12 @@ describe('createCheckoutAlgorithm', () => {
374
465
  });
375
466
 
376
467
  await expect(
377
- createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG)
468
+ createCheckoutAlgorithm(
469
+ BASE_REQUEST,
470
+ stripeProvider,
471
+ authProvider,
472
+ BILLING_CONFIG
473
+ )
378
474
  ).rejects.toThrow('Too many requests');
379
475
  });
380
476
 
@@ -386,7 +482,12 @@ describe('createCheckoutAlgorithm', () => {
386
482
  });
387
483
 
388
484
  await expect(
389
- createCheckoutAlgorithm(BASE_REQUEST, stripeProvider, authProvider, BILLING_CONFIG)
485
+ createCheckoutAlgorithm(
486
+ BASE_REQUEST,
487
+ stripeProvider,
488
+ authProvider,
489
+ BILLING_CONFIG
490
+ )
390
491
  ).rejects.toBe(unexpected);
391
492
  });
392
493
  });