@tuturuuu/utils 0.0.3 → 0.6.0

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 (186) hide show
  1. package/CHANGELOG.md +305 -0
  2. package/biome.json +5 -0
  3. package/jsr.json +8 -8
  4. package/package.json +63 -32
  5. package/src/__tests__/ai-temp-auth.test.ts +309 -0
  6. package/src/__tests__/api-proxy-guard.test.ts +1451 -0
  7. package/src/__tests__/app-url.test.ts +270 -0
  8. package/src/__tests__/avatar-url.test.ts +97 -0
  9. package/src/__tests__/color-helper.test.ts +179 -0
  10. package/src/__tests__/constants.test.ts +351 -0
  11. package/src/__tests__/crypto.test.ts +107 -0
  12. package/src/__tests__/date-helper.test.ts +408 -0
  13. package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
  14. package/src/__tests__/format.test.ts +317 -0
  15. package/src/__tests__/html-sanitizer.test.ts +360 -0
  16. package/src/__tests__/interest-calculator.test.ts +336 -0
  17. package/src/__tests__/interest-detector.test.ts +222 -0
  18. package/src/__tests__/label-colors.test.ts +241 -0
  19. package/src/__tests__/name-helper.test.ts +158 -0
  20. package/src/__tests__/node-diff.test.ts +576 -0
  21. package/src/__tests__/notification-service.test.ts +210 -0
  22. package/src/__tests__/onboarding-helper.test.ts +331 -0
  23. package/src/__tests__/path-helper.test.ts +152 -0
  24. package/src/__tests__/permissions.test.tsx +81 -0
  25. package/src/__tests__/request-emoji-limit.test.ts +172 -0
  26. package/src/__tests__/search-helper.test.ts +51 -0
  27. package/src/__tests__/storage-display-name.test.ts +37 -0
  28. package/src/__tests__/storage-path.test.ts +238 -0
  29. package/src/__tests__/tag-utils.test.ts +205 -0
  30. package/src/__tests__/task-description-yjs-state.test.ts +581 -0
  31. package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
  32. package/src/__tests__/task-helper-create-task.test.ts +129 -0
  33. package/src/__tests__/task-helpers.test.ts +464 -0
  34. package/src/__tests__/task-overrides.test.ts +305 -0
  35. package/src/__tests__/task-reorder-cache.test.ts +74 -0
  36. package/src/__tests__/task-sort-keys.test.ts +36 -0
  37. package/src/__tests__/task-transformers.test.ts +62 -0
  38. package/src/__tests__/text-helper.test.ts +776 -0
  39. package/src/__tests__/time-helper.test.ts +70 -0
  40. package/src/__tests__/time-tracker-period.test.ts +55 -0
  41. package/src/__tests__/timezone.test.ts +117 -0
  42. package/src/__tests__/upstash-rest.test.ts +77 -0
  43. package/src/__tests__/uuid-helper.test.ts +133 -0
  44. package/src/__tests__/workspace-helper.test.ts +859 -0
  45. package/src/__tests__/workspace-limits.test.ts +255 -0
  46. package/src/__tests__/yjs-helper.test.ts +581 -0
  47. package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
  48. package/src/abuse-protection/__tests__/edge.test.ts +136 -0
  49. package/src/abuse-protection/__tests__/index.test.ts +562 -0
  50. package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
  51. package/src/abuse-protection/backend-rate-limit.ts +44 -0
  52. package/src/abuse-protection/constants.ts +117 -0
  53. package/src/abuse-protection/edge.ts +223 -0
  54. package/src/abuse-protection/index.ts +1545 -0
  55. package/src/abuse-protection/reputation.ts +587 -0
  56. package/src/abuse-protection/types.ts +97 -0
  57. package/src/abuse-protection/user-agent.ts +124 -0
  58. package/src/abuse-protection/user-suspension.ts +231 -0
  59. package/src/ai-temp-auth.ts +315 -0
  60. package/src/api-proxy-guard.ts +965 -0
  61. package/src/app-url.ts +96 -0
  62. package/src/avatar-url.ts +64 -0
  63. package/src/break-duration.ts +84 -0
  64. package/src/calendar-auth-token.test.ts +37 -0
  65. package/src/calendar-auth-token.ts +19 -0
  66. package/src/calendar-sync-coordination.md +197 -0
  67. package/src/calendar-utils.test.ts +169 -0
  68. package/src/calendar-utils.ts +91 -0
  69. package/src/color-helper.ts +110 -0
  70. package/src/common/nextjs.tsx +99 -0
  71. package/src/common/scan.tsx +15 -0
  72. package/src/configs/reports.ts +160 -0
  73. package/src/constants.ts +85 -0
  74. package/src/crypto.ts +21 -0
  75. package/src/currencies.ts +97 -0
  76. package/src/date-helper.ts +313 -0
  77. package/src/editor/convert-to-task.ts +264 -0
  78. package/src/editor/index.ts +5 -0
  79. package/src/email/__tests__/client.test.ts +141 -0
  80. package/src/email/__tests__/validation.test.ts +46 -0
  81. package/src/email/client.ts +92 -0
  82. package/src/email/server.ts +128 -0
  83. package/src/email/validation.ts +11 -0
  84. package/src/encryption/__tests__/calendar-events.test.ts +411 -0
  85. package/src/encryption/__tests__/configuration.test.ts +114 -0
  86. package/src/encryption/__tests__/field-encryption.test.ts +232 -0
  87. package/src/encryption/__tests__/key-generation.test.ts +30 -0
  88. package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
  89. package/src/encryption/__tests__/test-helpers.ts +22 -0
  90. package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
  91. package/src/encryption/encryption-service.ts +343 -0
  92. package/src/encryption/index.ts +25 -0
  93. package/src/encryption/types.ts +57 -0
  94. package/src/exchange-rates.ts +49 -0
  95. package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
  96. package/src/feature-flags/core.ts +322 -0
  97. package/src/feature-flags/data.ts +16 -0
  98. package/src/feature-flags/default.ts +18 -0
  99. package/src/feature-flags/index.ts +7 -0
  100. package/src/feature-flags/requestable-features.ts +79 -0
  101. package/src/feature-flags/types.ts +4 -0
  102. package/src/fetcher.ts +2 -0
  103. package/src/finance/index.ts +4 -0
  104. package/src/finance/interest-calculator.ts +456 -0
  105. package/src/finance/interest-detector.ts +141 -0
  106. package/src/finance/transform-invoice-results.ts +219 -0
  107. package/src/finance/wallet-permissions.test.ts +169 -0
  108. package/src/finance/wallet-permissions.ts +82 -0
  109. package/src/format.ts +120 -1
  110. package/src/generated/platform-build-metadata.ts +11 -0
  111. package/src/hooks/use-platform.ts +64 -0
  112. package/src/html-sanitizer.ts +155 -0
  113. package/src/internal-domains.ts +497 -0
  114. package/src/keyboard-preset.ts +109 -0
  115. package/src/label-colors.ts +213 -0
  116. package/src/launchable-apps.test.ts +126 -0
  117. package/src/launchable-apps.ts +490 -0
  118. package/src/name-helper.ts +269 -0
  119. package/src/next-config.test.ts +234 -0
  120. package/src/next-config.ts +203 -0
  121. package/src/node-diff.ts +375 -0
  122. package/src/notification-service.ts +379 -0
  123. package/src/nova/scores/__tests__/calculate.test.ts +254 -0
  124. package/src/nova/scores/calculate.ts +132 -0
  125. package/src/nova/submissions/check-permission.ts +132 -0
  126. package/src/onboarding-helper.ts +213 -0
  127. package/src/path-helper.ts +93 -0
  128. package/src/permissions.tsx +1170 -0
  129. package/src/plan-helpers.test.ts +188 -0
  130. package/src/plan-helpers.ts +80 -0
  131. package/src/platform-release.test.ts +74 -0
  132. package/src/platform-release.ts +155 -0
  133. package/src/portless.ts +124 -0
  134. package/src/priority-styles.ts +42 -0
  135. package/src/request-emoji-limit.ts +335 -0
  136. package/src/search-helper.ts +18 -0
  137. package/src/search.test.ts +89 -0
  138. package/src/search.ts +355 -0
  139. package/src/storage-display-name.ts +30 -0
  140. package/src/storage-path.ts +147 -0
  141. package/src/tag-utils.ts +159 -0
  142. package/src/task/reorder.ts +245 -0
  143. package/src/task/transformers.ts +149 -0
  144. package/src/task-date-timezone.ts +133 -0
  145. package/src/task-description-content.ts +240 -0
  146. package/src/task-helper/board.ts +193 -0
  147. package/src/task-helper/bulk-actions.ts +564 -0
  148. package/src/task-helper/personal-external-staging.ts +21 -0
  149. package/src/task-helper/recycle-bin.ts +202 -0
  150. package/src/task-helper/relationships.ts +346 -0
  151. package/src/task-helper/shared.ts +109 -0
  152. package/src/task-helper/sort-keys.ts +337 -0
  153. package/src/task-helper/task-hooks-basic.ts +342 -0
  154. package/src/task-helper/task-hooks-move.ts +264 -0
  155. package/src/task-helper/task-operations.ts +278 -0
  156. package/src/task-helper.ts +12 -0
  157. package/src/task-helpers.ts +241 -0
  158. package/src/task-list-status.ts +62 -0
  159. package/src/task-overrides.ts +82 -0
  160. package/src/task-snapshot.ts +374 -0
  161. package/src/text-diff.ts +81 -0
  162. package/src/text-helper.ts +537 -0
  163. package/src/time-helper.ts +63 -0
  164. package/src/time-tracker-period.ts +73 -0
  165. package/src/timeblock-helper.ts +418 -0
  166. package/src/timezone.ts +190 -0
  167. package/src/timezones.json +1271 -0
  168. package/src/upstash-rest.ts +56 -0
  169. package/src/user-helper.ts +296 -0
  170. package/src/uuid-helper.ts +11 -0
  171. package/src/workspace-handle.ts +10 -0
  172. package/src/workspace-helper.ts +1408 -0
  173. package/src/workspace-limits.ts +68 -0
  174. package/src/yjs-helper.ts +217 -0
  175. package/src/yjs-task-description.ts +81 -0
  176. package/tsconfig.json +3 -5
  177. package/tsconfig.typecheck.json +33 -0
  178. package/vitest.config.ts +36 -0
  179. package/dist/index.d.ts +0 -8
  180. package/dist/index.js +0 -2
  181. package/dist/index.js.map +0 -1
  182. package/dist/index.mjs +0 -2
  183. package/dist/index.mjs.map +0 -1
  184. package/eslint.config.mjs +0 -20
  185. package/rollup.config.js +0 -41
  186. package/src/index.ts +0 -1
@@ -0,0 +1,141 @@
1
+ import { describe, expect, it } from 'vitest';
2
+
3
+ import {
4
+ formatEmailAddresses,
5
+ isEmail,
6
+ isExactTuturuuuDotComEmail,
7
+ isIncompleteEmail,
8
+ isValidTuturuuuEmail,
9
+ suggestEmails,
10
+ } from '../client';
11
+
12
+ describe('formatEmailAddresses', () => {
13
+ it('parses name and email from formatted string', () => {
14
+ const result = formatEmailAddresses('Jane Doe <jane@tuturuuu.com>');
15
+
16
+ expect(result).toEqual([
17
+ {
18
+ name: 'Jane Doe',
19
+ email: 'jane@tuturuuu.com',
20
+ raw: 'Jane Doe <jane@tuturuuu.com>',
21
+ },
22
+ ]);
23
+ });
24
+
25
+ it('handles simple email without display name', () => {
26
+ const result = formatEmailAddresses('user@example.com');
27
+
28
+ expect(result).toEqual([
29
+ {
30
+ name: '',
31
+ email: 'user@example.com',
32
+ raw: 'user@example.com',
33
+ },
34
+ ]);
35
+ });
36
+
37
+ it('returns fallback object when value is not an email', () => {
38
+ const result = formatEmailAddresses('not-an-email');
39
+
40
+ expect(result).toEqual([
41
+ {
42
+ name: '',
43
+ email: '',
44
+ raw: 'not-an-email',
45
+ },
46
+ ]);
47
+ });
48
+
49
+ it('filters out non-string values when provided in an array', () => {
50
+ const result = formatEmailAddresses([
51
+ 'Alice <alice@example.com>',
52
+ 123 as unknown as string,
53
+ ]);
54
+
55
+ expect(result).toEqual([
56
+ {
57
+ name: 'Alice',
58
+ email: 'alice@example.com',
59
+ raw: 'Alice <alice@example.com>',
60
+ },
61
+ ]);
62
+ });
63
+
64
+ it('returns an empty array for falsy input', () => {
65
+ expect(formatEmailAddresses(undefined)).toEqual([]);
66
+ });
67
+ });
68
+
69
+ describe('isValidTuturuuuEmail', () => {
70
+ it('accepts first-party tuturuuu domains', () => {
71
+ expect(isValidTuturuuuEmail('member@tuturuuu.com')).toBe(true);
72
+ expect(isValidTuturuuuEmail('member@xwf.tuturuuu.com')).toBe(true);
73
+ });
74
+
75
+ it('rejects other domains and falsy values', () => {
76
+ expect(isValidTuturuuuEmail('member@example.com')).toBe(false);
77
+ expect(isValidTuturuuuEmail('member@sub.tuturuuu.com')).toBe(false);
78
+ expect(isValidTuturuuuEmail(null)).toBe(false);
79
+ expect(isValidTuturuuuEmail(undefined)).toBe(false);
80
+ });
81
+ });
82
+
83
+ describe('isExactTuturuuuDotComEmail', () => {
84
+ it('accepts only exact tuturuuu.com accounts', () => {
85
+ expect(isExactTuturuuuDotComEmail('member@tuturuuu.com')).toBe(true);
86
+ expect(isExactTuturuuuDotComEmail('MEMBER@TUTURUUU.COM')).toBe(true);
87
+ expect(isExactTuturuuuDotComEmail('member@xwf.tuturuuu.com')).toBe(false);
88
+ expect(isExactTuturuuuDotComEmail('member@sub.tuturuuu.com')).toBe(false);
89
+ expect(isExactTuturuuuDotComEmail('member@example.com')).toBe(false);
90
+ });
91
+ });
92
+
93
+ describe('isEmail', () => {
94
+ it('validates complex email formats', () => {
95
+ expect(isEmail('user.name+alias@example.co.uk')).toBe(true);
96
+ });
97
+
98
+ it('rejects malformed values', () => {
99
+ expect(isEmail('missing-at-symbol.com')).toBe(false);
100
+ expect(isEmail(' user@example.com')).toBe(false);
101
+ });
102
+ });
103
+
104
+ describe('isIncompleteEmail', () => {
105
+ it('detects emails missing domain details', () => {
106
+ expect(isIncompleteEmail('user@domain')).toBe(true);
107
+ expect(isIncompleteEmail('user@domain c')).toBe(true);
108
+ });
109
+
110
+ it('rejects complete or invalid starts', () => {
111
+ expect(isIncompleteEmail('user@domain.com')).toBe(false);
112
+ expect(isIncompleteEmail('@domain.com')).toBe(false);
113
+ expect(isIncompleteEmail('')).toBe(false);
114
+ });
115
+ });
116
+
117
+ describe('suggestEmails', () => {
118
+ it('returns provider list when no handle provided', () => {
119
+ expect(suggestEmails('')).toEqual([
120
+ '@gmail.com',
121
+ '@yahoo.com',
122
+ '@outlook.com',
123
+ '@tuturuuu.com',
124
+ ]);
125
+ });
126
+
127
+ it('uses the provided handle when available', () => {
128
+ expect(suggestEmails('handle')).toEqual([
129
+ 'handle@gmail.com',
130
+ 'handle@yahoo.com',
131
+ 'handle@outlook.com',
132
+ 'handle@tuturuuu.com',
133
+ ]);
134
+ expect(suggestEmails('handle@existing.com')).toEqual([
135
+ 'handle@gmail.com',
136
+ 'handle@yahoo.com',
137
+ 'handle@outlook.com',
138
+ 'handle@tuturuuu.com',
139
+ ]);
140
+ });
141
+ });
@@ -0,0 +1,46 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ DOMAIN_BLACKLIST_REGEX,
4
+ EMAIL_BLACKLIST_REGEX,
5
+ isValidBlacklistDomain,
6
+ isValidBlacklistEmail,
7
+ } from '../validation';
8
+
9
+ describe('email blacklist validation utils', () => {
10
+ describe('isValidBlacklistEmail', () => {
11
+ it('accepts well-formed email addresses with TLD', () => {
12
+ expect(isValidBlacklistEmail('user@example.com')).toBe(true);
13
+ expect(isValidBlacklistEmail('first.last+tag@sub.domain.io')).toBe(true);
14
+ });
15
+
16
+ it('rejects email addresses without TLD', () => {
17
+ expect(isValidBlacklistEmail('user@example')).toBe(false);
18
+ });
19
+
20
+ it('mirrors EMAIL_BLACKLIST_REGEX', () => {
21
+ const sample = 'name+alias@sub.domain.com';
22
+ expect(isValidBlacklistEmail(sample)).toBe(
23
+ EMAIL_BLACKLIST_REGEX.test(sample)
24
+ );
25
+ });
26
+ });
27
+
28
+ describe('isValidBlacklistDomain', () => {
29
+ it('accepts well-formed domains', () => {
30
+ expect(isValidBlacklistDomain('example.com')).toBe(true);
31
+ expect(isValidBlacklistDomain('sub.domain.co')).toBe(true);
32
+ });
33
+
34
+ it('rejects malformed domains', () => {
35
+ expect(isValidBlacklistDomain('-example.com')).toBe(false);
36
+ expect(isValidBlacklistDomain('example')).toBe(false);
37
+ });
38
+
39
+ it('mirrors DOMAIN_BLACKLIST_REGEX', () => {
40
+ const sample = 'valid-domain.org';
41
+ expect(isValidBlacklistDomain(sample)).toBe(
42
+ DOMAIN_BLACKLIST_REGEX.test(sample)
43
+ );
44
+ });
45
+ });
46
+ });
@@ -0,0 +1,92 @@
1
+ export function formatEmailAddresses(
2
+ addresses?: string | string[]
3
+ ): { name: string; email: string; raw: string }[] {
4
+ if (!addresses) return [];
5
+ const arr = Array.isArray(addresses) ? addresses : [addresses];
6
+ return arr
7
+ .filter((addr): addr is string => typeof addr === 'string')
8
+ .map((addr) => {
9
+ const match = addr.match(
10
+ /^(.+?)\s*<\s*([\w\-.+]+@[\w\-.]+\.[a-zA-Z]{2,})\s*>$/
11
+ );
12
+ if (match) {
13
+ return { name: match[1] ?? '', email: match[2] ?? '', raw: addr };
14
+ }
15
+ // If just an email
16
+ if (/^[\w\-.+]+@[\w\-.]+\.[a-zA-Z]{2,}$/.test(addr)) {
17
+ return { name: '', email: addr, raw: addr };
18
+ }
19
+ return { name: '', email: '', raw: addr };
20
+ });
21
+ }
22
+
23
+ export const isValidTuturuuuEmail = (email?: string | null): boolean => {
24
+ if (!email) return false;
25
+ const emailRegex = /^[^\s@]+@(tuturuuu\.com|xwf\.tuturuuu\.com)$/;
26
+ return emailRegex.test(email);
27
+ };
28
+
29
+ export const isExactTuturuuuDotComEmail = (email?: string | null): boolean => {
30
+ if (!email) return false;
31
+ return /^[^\s@]+@tuturuuu\.com$/i.test(email.trim());
32
+ };
33
+
34
+ export const isEmail = (text: string): boolean => {
35
+ const emailRegex =
36
+ /^(?:[^<>()[\]\\.,;:\s@"]+(?:\.[^<>()[\]\\.,;:\s@"]+)*|".+")@(?:\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}]|(?:[a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,})$/;
37
+ return emailRegex.test(text);
38
+ };
39
+
40
+ export const isIncompleteEmail = (text: string): boolean => {
41
+ if (!text) return false;
42
+
43
+ // Find the @ symbol
44
+ const atIndex = text.indexOf('@');
45
+ if (atIndex === -1) return false;
46
+
47
+ // Split into local and domain parts
48
+ const localPart = text.slice(0, atIndex);
49
+ const domainPart = text.slice(atIndex + 1);
50
+
51
+ // Return false for cases that are definitely not valid email starts
52
+ if (!localPart || localPart.endsWith(' ') || text.startsWith('@')) {
53
+ return false;
54
+ }
55
+
56
+ // Return true if domain part has spaces or is incomplete
57
+ if (domainPart.includes(' ') || !domainPart.includes('.')) {
58
+ return true;
59
+ }
60
+
61
+ // Return false if it has a valid domain part
62
+ if (/^[^.\s]+\.[^.\s]+$/.test(domainPart)) {
63
+ return false;
64
+ }
65
+
66
+ return true;
67
+ };
68
+
69
+ export const suggestEmails = (text: string): string[] => {
70
+ if (!text) {
71
+ return ['@gmail.com', '@yahoo.com', '@outlook.com', '@tuturuuu.com'];
72
+ }
73
+
74
+ const handle = text.split('@')[0];
75
+ const suggestions = [
76
+ `${handle}@gmail.com`,
77
+ `${handle}@yahoo.com`,
78
+ `${handle}@outlook.com`,
79
+ `${handle}@tuturuuu.com`,
80
+ ];
81
+
82
+ return suggestions;
83
+ };
84
+
85
+ export function generateEmailSubaddressing(email: string, wsId: string) {
86
+ const atIndex = email.indexOf('@');
87
+ if (atIndex === -1) return email; // Invalid email, return as is
88
+
89
+ const localPart = email.substring(0, atIndex);
90
+ const domainPart = email.substring(atIndex);
91
+ return `${localPart}+ws_${wsId}${domainPart}`;
92
+ }
@@ -0,0 +1,128 @@
1
+ import { createAdminClient } from '@tuturuuu/supabase/next/server';
2
+ import { hashEmail } from '../abuse-protection';
3
+
4
+ export const validateEmail = async (email?: string | null) => {
5
+ if (!email) throw 'Email is required';
6
+
7
+ const regex = /\S+@\S+\.\S+/;
8
+ if (!regex.test(email)) throw 'Email is invalid';
9
+
10
+ return email.toLowerCase();
11
+ };
12
+
13
+ export interface EmailInfrastructureBlockResult {
14
+ isBlocked: boolean;
15
+ reason?: string;
16
+ blockType?: 'blacklist' | 'bounce' | 'complaint';
17
+ }
18
+
19
+ /**
20
+ * Check if an email is blocked by infrastructure.
21
+ * This checks both:
22
+ * 1. Email blacklist (blocked emails/domains)
23
+ * 2. Email bounce/complaint history (hard bounces, spam complaints)
24
+ *
25
+ * Use this before sending OTP or transactional emails to avoid
26
+ * sending to known-bad addresses and hurting sender reputation.
27
+ */
28
+ export const checkEmailInfrastructureBlocked = async (
29
+ email: string
30
+ ): Promise<EmailInfrastructureBlockResult> => {
31
+ try {
32
+ const sbAdmin = await createAdminClient();
33
+ const normalizedEmail = email.toLowerCase();
34
+ const emailHash = hashEmail(normalizedEmail);
35
+
36
+ // Check email blacklist first (direct blocks)
37
+ const { data: isBlacklisted, error: blacklistError } = await sbAdmin.rpc(
38
+ 'check_email_blocked',
39
+ { p_email: normalizedEmail }
40
+ );
41
+
42
+ if (blacklistError) {
43
+ console.error(
44
+ '[EmailInfrastructureCheck] Blacklist check error:',
45
+ blacklistError
46
+ );
47
+ // Fail open - don't block if we can't check
48
+ } else if (isBlacklisted) {
49
+ console.log(
50
+ `[EmailInfrastructureCheck] Email ${normalizedEmail} is blacklisted`
51
+ );
52
+ return {
53
+ isBlocked: true,
54
+ reason: 'Email address is blocked',
55
+ blockType: 'blacklist',
56
+ };
57
+ }
58
+
59
+ // Check bounce/complaint status
60
+ const { data: bounceStatus, error: bounceError } = await sbAdmin.rpc(
61
+ 'check_email_bounce_status',
62
+ { p_email_hash: emailHash }
63
+ );
64
+
65
+ if (bounceError) {
66
+ console.error(
67
+ '[EmailInfrastructureCheck] Bounce check error:',
68
+ bounceError
69
+ );
70
+ // Fail open - don't block if we can't check
71
+ } else if (bounceStatus && bounceStatus.length > 0) {
72
+ const status = bounceStatus[0];
73
+ if (status?.is_blocked) {
74
+ console.log(
75
+ `[EmailInfrastructureCheck] Email ${normalizedEmail} blocked due to bounces/complaints`
76
+ );
77
+ return {
78
+ isBlocked: true,
79
+ reason: status.block_reason || 'Email has delivery issues',
80
+ blockType:
81
+ status.complaint_count && status.complaint_count > 0
82
+ ? 'complaint'
83
+ : 'bounce',
84
+ };
85
+ }
86
+ }
87
+
88
+ return { isBlocked: false };
89
+ } catch (error) {
90
+ console.error('[EmailInfrastructureCheck] Unexpected error:', error);
91
+ // Fail open - don't block if we can't check
92
+ return { isBlocked: false };
93
+ }
94
+ };
95
+
96
+ export const validateOtp = async (otp?: string | null) => {
97
+ if (!otp) throw 'OTP is required';
98
+
99
+ const regex = /^\d{6}$/;
100
+ if (!regex.test(otp)) throw 'OTP is invalid';
101
+
102
+ return otp;
103
+ };
104
+
105
+ export const checkIfUserExists = async ({ email }: { email: string }) => {
106
+ const sbAdmin = await createAdminClient();
107
+
108
+ const { data, error } = await sbAdmin
109
+ .from('user_private_details')
110
+ .select('id:user_id')
111
+ .eq('email', email)
112
+ .maybeSingle();
113
+
114
+ if (error) throw error.message;
115
+ return data?.id;
116
+ };
117
+
118
+ export const generateRandomPassword = () => {
119
+ const length = 16;
120
+ const charset =
121
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+~`|}{[]:;?><,./-=';
122
+
123
+ let temp = '';
124
+ for (let i = 0, n = charset.length; i < length; ++i)
125
+ temp += charset.charAt(Math.floor(Math.random() * n));
126
+
127
+ return temp;
128
+ };
@@ -0,0 +1,11 @@
1
+ export const EMAIL_BLACKLIST_REGEX =
2
+ /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
3
+
4
+ export const DOMAIN_BLACKLIST_REGEX =
5
+ /^(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/;
6
+
7
+ export const isValidBlacklistEmail = (value: string): boolean =>
8
+ EMAIL_BLACKLIST_REGEX.test(value);
9
+
10
+ export const isValidBlacklistDomain = (value: string): boolean =>
11
+ DOMAIN_BLACKLIST_REGEX.test(value);