@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
@@ -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
  });
@@ -80,7 +80,10 @@ vi.mock('../../logger.js', () => ({
80
80
  // Reset idempotency store between tests so events are never pre-marked
81
81
  import { resetIdempotencyStore } from '../idempotency.js';
82
82
  import { processWebhook } from '../webhookHandler.js';
83
- import type { UpdateSubscriptionFn, WebhookAuthProvider } from '../webhookHandler.js';
83
+ import type {
84
+ UpdateSubscriptionFn,
85
+ WebhookAuthProvider,
86
+ } from '../webhookHandler.js';
84
87
 
85
88
  // ---------------------------------------------------------------------------
86
89
  // Shared test fixtures
@@ -217,15 +220,15 @@ describe('processWebhook — signature verification', () => {
217
220
  const stripe = {
218
221
  webhooks: {
219
222
  constructEvent: vi.fn(() => {
220
- throw new Error('No signatures found matching the expected signature');
223
+ throw new Error(
224
+ 'No signatures found matching the expected signature'
225
+ );
221
226
  }),
222
227
  },
223
228
  subscriptions: { retrieve: vi.fn() },
224
229
  };
225
230
 
226
- await expect(
227
- callProcessWebhook({ stripe })
228
- ).rejects.toThrow();
231
+ await expect(callProcessWebhook({ stripe })).rejects.toThrow();
229
232
  });
230
233
  });
231
234
 
@@ -260,7 +263,10 @@ describe('processWebhook — idempotency', () => {
260
263
  updateSubscription: updateSubscription2,
261
264
  });
262
265
 
263
- expect(result).toEqual({ success: true, message: 'Event already processed' });
266
+ expect(result).toEqual({
267
+ success: true,
268
+ message: 'Event already processed',
269
+ });
264
270
  expect(updateSubscription2).not.toHaveBeenCalled();
265
271
  });
266
272
 
@@ -279,8 +285,16 @@ describe('processWebhook — idempotency', () => {
279
285
  const updateSubscription1 = makeUpdateSubscription();
280
286
  const updateSubscription2 = makeUpdateSubscription();
281
287
 
282
- await callProcessWebhook({ stripe: makeStripe(event1), config, updateSubscription: updateSubscription1 });
283
- await callProcessWebhook({ stripe: makeStripe(event2), config, updateSubscription: updateSubscription2 });
288
+ await callProcessWebhook({
289
+ stripe: makeStripe(event1),
290
+ config,
291
+ updateSubscription: updateSubscription1,
292
+ });
293
+ await callProcessWebhook({
294
+ stripe: makeStripe(event2),
295
+ config,
296
+ updateSubscription: updateSubscription2,
297
+ });
284
298
 
285
299
  expect(updateSubscription1).toHaveBeenCalledTimes(1);
286
300
  expect(updateSubscription2).toHaveBeenCalledTimes(1);
@@ -349,7 +363,11 @@ describe('event routing — checkout.session.completed (StripeSubscription)', ()
349
363
  const config = makeConfig();
350
364
  const updateSubscription = makeUpdateSubscription();
351
365
 
352
- await callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription });
366
+ await callProcessWebhook({
367
+ stripe: makeStripe(event),
368
+ config,
369
+ updateSubscription,
370
+ });
353
371
 
354
372
  expect(updateSubscription).toHaveBeenCalledOnce();
355
373
  const [userId, data] = (updateSubscription as any).mock.calls[0];
@@ -370,9 +388,16 @@ describe('event routing — checkout.session.completed (StripeSubscription)', ()
370
388
  customer: 'cus_hook',
371
389
  });
372
390
 
373
- await callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription: makeUpdateSubscription() });
391
+ await callProcessWebhook({
392
+ stripe: makeStripe(event),
393
+ config,
394
+ updateSubscription: makeUpdateSubscription(),
395
+ });
374
396
 
375
- expect(onSubscriptionCreated).toHaveBeenCalledWith('u_hook', expect.any(Object));
397
+ expect(onSubscriptionCreated).toHaveBeenCalledWith(
398
+ 'u_hook',
399
+ expect.any(Object)
400
+ );
376
401
  });
377
402
 
378
403
  it('does NOT throw when onSubscriptionCreated hook throws (non-critical)', async () => {
@@ -387,7 +412,11 @@ describe('event routing — checkout.session.completed (StripeSubscription)', ()
387
412
  });
388
413
 
389
414
  await expect(
390
- callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription: makeUpdateSubscription() })
415
+ callProcessWebhook({
416
+ stripe: makeStripe(event),
417
+ config,
418
+ updateSubscription: makeUpdateSubscription(),
419
+ })
391
420
  ).resolves.toEqual({ success: true, message: 'Webhook processed' });
392
421
  });
393
422
 
@@ -459,7 +488,11 @@ describe('event routing — checkout.session.completed (StripePayment)', () => {
459
488
  const config = makeConfig();
460
489
  const updateSubscription = makeUpdateSubscription();
461
490
 
462
- await callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription });
491
+ await callProcessWebhook({
492
+ stripe: makeStripe(event),
493
+ config,
494
+ updateSubscription,
495
+ });
463
496
 
464
497
  expect(updateSubscription).toHaveBeenCalledOnce();
465
498
  const [userId, data] = (updateSubscription as any).mock.calls[0];
@@ -486,9 +519,16 @@ describe('event routing — checkout.session.completed (StripePayment)', () => {
486
519
  customer: 'cus_pay_hook',
487
520
  });
488
521
 
489
- await callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription: makeUpdateSubscription() });
522
+ await callProcessWebhook({
523
+ stripe: makeStripe(event),
524
+ config,
525
+ updateSubscription: makeUpdateSubscription(),
526
+ });
490
527
 
491
- expect(onPurchaseSuccess).toHaveBeenCalledWith('u_pay_hook', expect.any(Object));
528
+ expect(onPurchaseSuccess).toHaveBeenCalledWith(
529
+ 'u_pay_hook',
530
+ expect.any(Object)
531
+ );
492
532
  });
493
533
 
494
534
  it('does NOT throw when onPurchaseSuccess hook throws (non-critical)', async () => {
@@ -509,7 +549,11 @@ describe('event routing — checkout.session.completed (StripePayment)', () => {
509
549
  });
510
550
 
511
551
  await expect(
512
- callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription: makeUpdateSubscription() })
552
+ callProcessWebhook({
553
+ stripe: makeStripe(event),
554
+ config,
555
+ updateSubscription: makeUpdateSubscription(),
556
+ })
513
557
  ).resolves.toEqual({ success: true, message: 'Webhook processed' });
514
558
  });
515
559
  });
@@ -568,9 +612,16 @@ describe('event routing — invoice.payment_succeeded', () => {
568
612
  metadata: { userId: 'u_hook_renewal', billingConfigKey: 'pro_monthly' },
569
613
  }));
570
614
 
571
- await callProcessWebhook({ stripe, config, updateSubscription: makeUpdateSubscription() });
615
+ await callProcessWebhook({
616
+ stripe,
617
+ config,
618
+ updateSubscription: makeUpdateSubscription(),
619
+ });
572
620
 
573
- expect(onSubscriptionRenewed).toHaveBeenCalledWith('u_hook_renewal', expect.any(Object));
621
+ expect(onSubscriptionRenewed).toHaveBeenCalledWith(
622
+ 'u_hook_renewal',
623
+ expect.any(Object)
624
+ );
574
625
  });
575
626
 
576
627
  it('does NOT throw when onSubscriptionRenewed hook throws (non-critical)', async () => {
@@ -593,7 +644,11 @@ describe('event routing — invoice.payment_succeeded', () => {
593
644
  }));
594
645
 
595
646
  await expect(
596
- callProcessWebhook({ stripe, config, updateSubscription: makeUpdateSubscription() })
647
+ callProcessWebhook({
648
+ stripe,
649
+ config,
650
+ updateSubscription: makeUpdateSubscription(),
651
+ })
597
652
  ).resolves.toEqual({ success: true, message: 'Webhook processed' });
598
653
  });
599
654
 
@@ -647,7 +702,10 @@ describe('event routing — invoice.payment_succeeded', () => {
647
702
  const stripe = makeStripe(event);
648
703
  stripe.subscriptions.retrieve = vi.fn(async () => ({
649
704
  id: 'sub_payment_type',
650
- metadata: { userId: 'u_payment_type', billingConfigKey: 'lifetime_purchase' },
705
+ metadata: {
706
+ userId: 'u_payment_type',
707
+ billingConfigKey: 'lifetime_purchase',
708
+ },
651
709
  }));
652
710
  const updateSubscription = makeUpdateSubscription();
653
711
 
@@ -723,9 +781,16 @@ describe('event routing — customer.subscription.deleted', () => {
723
781
  };
724
782
  const event = makeEvent('customer.subscription.deleted', subscription);
725
783
 
726
- await callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription: makeUpdateSubscription() });
784
+ await callProcessWebhook({
785
+ stripe: makeStripe(event),
786
+ config,
787
+ updateSubscription: makeUpdateSubscription(),
788
+ });
727
789
 
728
- expect(onSubscriptionCancelled).toHaveBeenCalledWith('u_cancel_hook', expect.any(Object));
790
+ expect(onSubscriptionCancelled).toHaveBeenCalledWith(
791
+ 'u_cancel_hook',
792
+ expect.any(Object)
793
+ );
729
794
  });
730
795
 
731
796
  it('does NOT throw when onSubscriptionCancelled hook throws (non-critical)', async () => {
@@ -744,7 +809,11 @@ describe('event routing — customer.subscription.deleted', () => {
744
809
  const event = makeEvent('customer.subscription.deleted', subscription);
745
810
 
746
811
  await expect(
747
- callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription: makeUpdateSubscription() })
812
+ callProcessWebhook({
813
+ stripe: makeStripe(event),
814
+ config,
815
+ updateSubscription: makeUpdateSubscription(),
816
+ })
748
817
  ).resolves.toEqual({ success: true, message: 'Webhook processed' });
749
818
  });
750
819
 
@@ -776,7 +845,11 @@ describe('event routing — customer.subscription.deleted', () => {
776
845
  const event = makeEvent('customer.subscription.deleted', subscription);
777
846
  const updateSubscription = makeUpdateSubscription();
778
847
 
779
- await callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription });
848
+ await callProcessWebhook({
849
+ stripe: makeStripe(event),
850
+ config,
851
+ updateSubscription,
852
+ });
780
853
 
781
854
  expect(updateSubscription).not.toHaveBeenCalled();
782
855
  });
@@ -824,7 +897,10 @@ describe('event routing — invoice.payment_failed', () => {
824
897
 
825
898
  await callProcessWebhook({ stripe: makeStripe(event), config });
826
899
 
827
- expect(onPurchaseFailure).toHaveBeenCalledWith('u_fail_pay', expect.any(Object));
900
+ expect(onPurchaseFailure).toHaveBeenCalledWith(
901
+ 'u_fail_pay',
902
+ expect.any(Object)
903
+ );
828
904
  });
829
905
 
830
906
  it('calls onPaymentFailed for StripeSubscription billing items', async () => {
@@ -839,7 +915,10 @@ describe('event routing — invoice.payment_failed', () => {
839
915
 
840
916
  await callProcessWebhook({ stripe: makeStripe(event), config });
841
917
 
842
- expect(onPaymentFailed).toHaveBeenCalledWith('u_fail_sub', expect.any(Object));
918
+ expect(onPaymentFailed).toHaveBeenCalledWith(
919
+ 'u_fail_sub',
920
+ expect.any(Object)
921
+ );
843
922
  });
844
923
 
845
924
  it('does NOT throw when onPurchaseFailure hook throws (non-critical)', async () => {
@@ -922,7 +1001,11 @@ describe('event routing — invoice.payment_failed', () => {
922
1001
  const event = makeEvent('invoice.payment_failed', invoice);
923
1002
  const updateSubscription = makeUpdateSubscription();
924
1003
 
925
- await callProcessWebhook({ stripe: makeStripe(event), config, updateSubscription });
1004
+ await callProcessWebhook({
1005
+ stripe: makeStripe(event),
1006
+ config,
1007
+ updateSubscription,
1008
+ });
926
1009
 
927
1010
  expect(updateSubscription).not.toHaveBeenCalled();
928
1011
  });
@@ -952,7 +1035,9 @@ describe('event routing — unknown event types', () => {
952
1035
  });
953
1036
 
954
1037
  it('handles payment_intent.succeeded gracefully (not in router)', async () => {
955
- const event = makeEvent('payment_intent.succeeded', { id: 'pi_not_routed' });
1038
+ const event = makeEvent('payment_intent.succeeded', {
1039
+ id: 'pi_not_routed',
1040
+ });
956
1041
 
957
1042
  const result = await callProcessWebhook({ stripe: makeStripe(event) });
958
1043
 
@@ -997,7 +1082,10 @@ describe('state machine transitions', () => {
997
1082
 
998
1083
  it('checkout.session.completed → status=active (StripePayment)', async () => {
999
1084
  const event = makeEvent('checkout.session.completed', {
1000
- metadata: { userId: 'u_sm_pay_active', billingConfigKey: 'lifetime_purchase' },
1085
+ metadata: {
1086
+ userId: 'u_sm_pay_active',
1087
+ billingConfigKey: 'lifetime_purchase',
1088
+ },
1001
1089
  subscription: null,
1002
1090
  customer: 'cus_sm_pay_active',
1003
1091
  });
@@ -1086,6 +1174,9 @@ describe('processWebhook — return value', () => {
1086
1174
  await callProcessWebhook({ stripe: makeStripe(event) });
1087
1175
  const result = await callProcessWebhook({ stripe: makeStripe(event) });
1088
1176
 
1089
- expect(result).toEqual({ success: true, message: 'Event already processed' });
1177
+ expect(result).toEqual({
1178
+ success: true,
1179
+ message: 'Event already processed',
1180
+ });
1090
1181
  });
1091
1182
  });
@@ -9,7 +9,6 @@
9
9
  * @author AMBROISE PARK Consulting
10
10
  */
11
11
 
12
- import { HttpsError } from 'firebase-functions/v2/https';
13
12
  import * as v from 'valibot';
14
13
 
15
14
  import { DoNotDevError, EntityHookError } from '@donotdev/core/server';
@@ -26,7 +25,7 @@ export { DoNotDevError };
26
25
  * @since 0.0.1
27
26
  * @author AMBROISE PARK Consulting
28
27
  */
29
- export function handleError(error: unknown): never {
28
+ export async function handleError(error: unknown): Promise<never> {
30
29
  // W11: Log only in development to avoid double-logging with platform loggers.
31
30
  // Known DoNotDevError, ValiError, and EntityHookError are handled by callers.
32
31
  if (
@@ -97,7 +96,7 @@ export function handleError(error: unknown): never {
97
96
 
98
97
  // Platform-specific error throwing
99
98
  if (isFirebaseEnvironment()) {
100
- throwFirebaseError(code, message, details);
99
+ await throwFirebaseError(code, message, details);
101
100
  } else if (isVercelEnvironment()) {
102
101
  throwVercelError(code, message, details);
103
102
  } else {
@@ -143,12 +142,13 @@ function isVercelEnvironment(): boolean {
143
142
  * @since 0.0.1
144
143
  * @author AMBROISE PARK Consulting
145
144
  */
146
- function throwFirebaseError(
145
+ async function throwFirebaseError(
147
146
  code: string,
148
147
  message: string,
149
148
  details?: any
150
- ): never {
151
- // Static import - Firebase is always available in Firebase functions
149
+ ): Promise<never> {
150
+ // Dynamic import — firebase-functions is only loaded in Firebase environments
151
+ const { HttpsError } = await import('firebase-functions/v2/https');
152
152
  throw new HttpsError(code as any, message, details);
153
153
  }
154
154
 
@@ -212,4 +212,4 @@ export function prepareForFirestore<T = any>(
212
212
 
213
213
  // Return primitive values unchanged
214
214
  return data;
215
- }
215
+ }
@@ -19,8 +19,9 @@ export * from './oauth/index.js';
19
19
  export {
20
20
  createTimestamp,
21
21
  toTimestamp,
22
- toISOString,
23
22
  isTimestamp,
24
23
  transformFirestoreData,
25
24
  prepareForFirestore,
26
25
  } from './firebase.js';
26
+ // Note: toISOString is exported from ./utils/external/date.js (handles DateValue).
27
+ // The Firebase-specific toISOString (FirestoreTimestamp only) is available via direct import from ./firebase.js.
@@ -28,10 +28,8 @@ if (typeof globalThis !== 'undefined' && 'window' in globalThis) {
28
28
  throw new Error('Server logger cannot be imported on client side');
29
29
  }
30
30
 
31
- // ServerShim: Ensure we're in a Node.js environment
32
- if (typeof process === 'undefined' || !process.versions?.node) {
33
- throw new Error('Server logger requires Node.js environment');
34
- }
31
+ // ServerShim: Warn if not in Node.js (e.g., Deno) but don't throw — allow fallback logging
32
+ const _isNodeEnv = typeof process !== 'undefined' && !!process.versions?.node;
35
33
 
36
34
  let sentryEnabled = false;
37
35
  let sentryClient: any = null;
@@ -247,9 +245,13 @@ export const logger = {
247
245
  context,
248
246
  error: context?.error,
249
247
  metadata: {
250
- pid: process.pid,
251
- nodeVersion: process.version,
252
- platform: process.platform,
248
+ ...(_isNodeEnv
249
+ ? {
250
+ pid: process.pid,
251
+ nodeVersion: process.version,
252
+ platform: process.platform,
253
+ }
254
+ : {}),
253
255
  ...context?.metadata,
254
256
  },
255
257
  };