@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.
- package/package.json +4 -4
- package/src/firebase/auth/setCustomClaims.ts +19 -5
- package/src/firebase/baseFunction.ts +11 -3
- package/src/firebase/billing/changePlan.ts +5 -1
- package/src/firebase/billing/createCustomerPortal.ts +6 -2
- package/src/firebase/billing/webhookHandler.ts +4 -1
- package/src/firebase/crud/aggregate.ts +5 -1
- package/src/firebase/crud/create.ts +17 -4
- package/src/firebase/crud/list.ts +9 -4
- package/src/firebase/crud/update.ts +17 -4
- package/src/firebase/oauth/exchangeToken.ts +17 -4
- package/src/shared/__tests__/validation.test.ts +5 -3
- package/src/shared/billing/__tests__/createCheckout.test.ts +123 -22
- package/src/shared/billing/__tests__/webhookHandler.test.ts +121 -30
- package/src/shared/firebase.ts +1 -1
- package/src/shared/logger.ts +7 -1
- package/src/shared/oauth/__tests__/exchangeToken.test.ts +6 -8
- package/src/shared/oauth/__tests__/grantAccess.test.ts +31 -14
- package/src/shared/utils/internal/auth.ts +10 -3
- package/src/shared/utils/internal/rateLimiter.ts +8 -2
- package/src/shared/utils.ts +5 -1
- package/src/supabase/auth/deleteAccount.ts +1 -1
- package/src/supabase/auth/getCustomClaims.ts +5 -3
- package/src/supabase/auth/getUserAuthStatus.ts +5 -3
- package/src/supabase/auth/removeCustomClaims.ts +10 -5
- package/src/supabase/auth/setCustomClaims.ts +9 -4
- package/src/supabase/baseFunction.ts +77 -22
- package/src/supabase/billing/cancelSubscription.ts +9 -3
- package/src/supabase/billing/changePlan.ts +20 -5
- package/src/supabase/billing/createCheckoutSession.ts +20 -5
- package/src/supabase/billing/createCustomerPortal.ts +14 -4
- package/src/supabase/billing/refreshSubscriptionStatus.ts +29 -9
- package/src/supabase/crud/aggregate.ts +14 -4
- package/src/supabase/crud/create.ts +30 -11
- package/src/supabase/crud/delete.ts +11 -3
- package/src/supabase/crud/get.ts +25 -3
- package/src/supabase/crud/list.ts +76 -22
- package/src/supabase/crud/update.ts +32 -10
- package/src/supabase/helpers/authProvider.ts +5 -2
- package/src/supabase/index.ts +1 -4
- package/src/supabase/registerCrudFunctions.ts +11 -9
- package/src/supabase/utils/idempotency.ts +13 -15
- package/src/supabase/utils/monitoring.ts +5 -1
- package/src/supabase/utils/rateLimiter.ts +13 -3
- package/src/vercel/api/billing/webhook-handler.ts +6 -2
- package/src/vercel/api/crud/create.ts +7 -2
- package/src/vercel/api/crud/delete.ts +3 -1
- package/src/vercel/api/crud/get.ts +3 -1
- package/src/vercel/api/crud/list.ts +3 -1
- package/src/vercel/api/crud/update.ts +7 -2
|
@@ -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 {
|
|
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(
|
|
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({
|
|
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({
|
|
283
|
-
|
|
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({
|
|
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({
|
|
391
|
+
await callProcessWebhook({
|
|
392
|
+
stripe: makeStripe(event),
|
|
393
|
+
config,
|
|
394
|
+
updateSubscription: makeUpdateSubscription(),
|
|
395
|
+
});
|
|
374
396
|
|
|
375
|
-
expect(onSubscriptionCreated).toHaveBeenCalledWith(
|
|
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({
|
|
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({
|
|
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({
|
|
522
|
+
await callProcessWebhook({
|
|
523
|
+
stripe: makeStripe(event),
|
|
524
|
+
config,
|
|
525
|
+
updateSubscription: makeUpdateSubscription(),
|
|
526
|
+
});
|
|
490
527
|
|
|
491
|
-
expect(onPurchaseSuccess).toHaveBeenCalledWith(
|
|
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({
|
|
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({
|
|
615
|
+
await callProcessWebhook({
|
|
616
|
+
stripe,
|
|
617
|
+
config,
|
|
618
|
+
updateSubscription: makeUpdateSubscription(),
|
|
619
|
+
});
|
|
572
620
|
|
|
573
|
-
expect(onSubscriptionRenewed).toHaveBeenCalledWith(
|
|
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({
|
|
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: {
|
|
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({
|
|
784
|
+
await callProcessWebhook({
|
|
785
|
+
stripe: makeStripe(event),
|
|
786
|
+
config,
|
|
787
|
+
updateSubscription: makeUpdateSubscription(),
|
|
788
|
+
});
|
|
727
789
|
|
|
728
|
-
expect(onSubscriptionCancelled).toHaveBeenCalledWith(
|
|
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({
|
|
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({
|
|
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(
|
|
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(
|
|
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({
|
|
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', {
|
|
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: {
|
|
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({
|
|
1177
|
+
expect(result).toEqual({
|
|
1178
|
+
success: true,
|
|
1179
|
+
message: 'Event already processed',
|
|
1180
|
+
});
|
|
1090
1181
|
});
|
|
1091
1182
|
});
|
package/src/shared/firebase.ts
CHANGED
package/src/shared/logger.ts
CHANGED
|
@@ -245,7 +245,13 @@ export const logger = {
|
|
|
245
245
|
context,
|
|
246
246
|
error: context?.error,
|
|
247
247
|
metadata: {
|
|
248
|
-
...(_isNodeEnv
|
|
248
|
+
...(_isNodeEnv
|
|
249
|
+
? {
|
|
250
|
+
pid: process.pid,
|
|
251
|
+
nodeVersion: process.version,
|
|
252
|
+
platform: process.platform,
|
|
253
|
+
}
|
|
254
|
+
: {}),
|
|
249
255
|
...context?.metadata,
|
|
250
256
|
},
|
|
251
257
|
};
|
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect, vi } from 'vitest';
|
|
13
13
|
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
14
|
-
|
|
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({
|
|
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(
|
|
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(
|
|
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(
|
|
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();
|
|
@@ -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 (
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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, {
|
|
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),
|
package/src/shared/utils.ts
CHANGED
|
@@ -439,7 +439,11 @@ export async function findReferences(
|
|
|
439
439
|
}>;
|
|
440
440
|
}
|
|
441
441
|
): Promise<Array<{ collection: string; field: string; count: number }>> {
|
|
442
|
-
const references: Array<{
|
|
442
|
+
const references: Array<{
|
|
443
|
+
collection: string;
|
|
444
|
+
field: string;
|
|
445
|
+
count: number;
|
|
446
|
+
}> = [];
|
|
443
447
|
|
|
444
448
|
if (!referenceMetadata?.incoming?.length) {
|
|
445
449
|
return references;
|
|
@@ -43,14 +43,16 @@ export function createGetCustomClaims() {
|
|
|
43
43
|
'get-custom-claims',
|
|
44
44
|
getCustomClaimsSchema,
|
|
45
45
|
async (_data, ctx) => {
|
|
46
|
-
const {
|
|
47
|
-
|
|
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 {
|
|
49
|
-
|
|
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 {
|
|
53
|
-
|
|
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<
|
|
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 {
|
|
53
|
-
|
|
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<
|
|
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
|
}
|