@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.
- package/CHANGELOG.md +305 -0
- package/biome.json +5 -0
- package/jsr.json +8 -8
- package/package.json +63 -32
- package/src/__tests__/ai-temp-auth.test.ts +309 -0
- package/src/__tests__/api-proxy-guard.test.ts +1451 -0
- package/src/__tests__/app-url.test.ts +270 -0
- package/src/__tests__/avatar-url.test.ts +97 -0
- package/src/__tests__/color-helper.test.ts +179 -0
- package/src/__tests__/constants.test.ts +351 -0
- package/src/__tests__/crypto.test.ts +107 -0
- package/src/__tests__/date-helper.test.ts +408 -0
- package/src/__tests__/fixtures/task-description-full-featured.json +456 -0
- package/src/__tests__/format.test.ts +317 -0
- package/src/__tests__/html-sanitizer.test.ts +360 -0
- package/src/__tests__/interest-calculator.test.ts +336 -0
- package/src/__tests__/interest-detector.test.ts +222 -0
- package/src/__tests__/label-colors.test.ts +241 -0
- package/src/__tests__/name-helper.test.ts +158 -0
- package/src/__tests__/node-diff.test.ts +576 -0
- package/src/__tests__/notification-service.test.ts +210 -0
- package/src/__tests__/onboarding-helper.test.ts +331 -0
- package/src/__tests__/path-helper.test.ts +152 -0
- package/src/__tests__/permissions.test.tsx +81 -0
- package/src/__tests__/request-emoji-limit.test.ts +172 -0
- package/src/__tests__/search-helper.test.ts +51 -0
- package/src/__tests__/storage-display-name.test.ts +37 -0
- package/src/__tests__/storage-path.test.ts +238 -0
- package/src/__tests__/tag-utils.test.ts +205 -0
- package/src/__tests__/task-description-yjs-state.test.ts +581 -0
- package/src/__tests__/task-helper-board-api-routing.test.ts +94 -0
- package/src/__tests__/task-helper-create-task.test.ts +129 -0
- package/src/__tests__/task-helpers.test.ts +464 -0
- package/src/__tests__/task-overrides.test.ts +305 -0
- package/src/__tests__/task-reorder-cache.test.ts +74 -0
- package/src/__tests__/task-sort-keys.test.ts +36 -0
- package/src/__tests__/task-transformers.test.ts +62 -0
- package/src/__tests__/text-helper.test.ts +776 -0
- package/src/__tests__/time-helper.test.ts +70 -0
- package/src/__tests__/time-tracker-period.test.ts +55 -0
- package/src/__tests__/timezone.test.ts +117 -0
- package/src/__tests__/upstash-rest.test.ts +77 -0
- package/src/__tests__/uuid-helper.test.ts +133 -0
- package/src/__tests__/workspace-helper.test.ts +859 -0
- package/src/__tests__/workspace-limits.test.ts +255 -0
- package/src/__tests__/yjs-helper.test.ts +581 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +113 -0
- package/src/abuse-protection/__tests__/edge.test.ts +136 -0
- package/src/abuse-protection/__tests__/index.test.ts +562 -0
- package/src/abuse-protection/__tests__/reputation.test.ts +192 -0
- package/src/abuse-protection/backend-rate-limit.ts +44 -0
- package/src/abuse-protection/constants.ts +117 -0
- package/src/abuse-protection/edge.ts +223 -0
- package/src/abuse-protection/index.ts +1545 -0
- package/src/abuse-protection/reputation.ts +587 -0
- package/src/abuse-protection/types.ts +97 -0
- package/src/abuse-protection/user-agent.ts +124 -0
- package/src/abuse-protection/user-suspension.ts +231 -0
- package/src/ai-temp-auth.ts +315 -0
- package/src/api-proxy-guard.ts +965 -0
- package/src/app-url.ts +96 -0
- package/src/avatar-url.ts +64 -0
- package/src/break-duration.ts +84 -0
- package/src/calendar-auth-token.test.ts +37 -0
- package/src/calendar-auth-token.ts +19 -0
- package/src/calendar-sync-coordination.md +197 -0
- package/src/calendar-utils.test.ts +169 -0
- package/src/calendar-utils.ts +91 -0
- package/src/color-helper.ts +110 -0
- package/src/common/nextjs.tsx +99 -0
- package/src/common/scan.tsx +15 -0
- package/src/configs/reports.ts +160 -0
- package/src/constants.ts +85 -0
- package/src/crypto.ts +21 -0
- package/src/currencies.ts +97 -0
- package/src/date-helper.ts +313 -0
- package/src/editor/convert-to-task.ts +264 -0
- package/src/editor/index.ts +5 -0
- package/src/email/__tests__/client.test.ts +141 -0
- package/src/email/__tests__/validation.test.ts +46 -0
- package/src/email/client.ts +92 -0
- package/src/email/server.ts +128 -0
- package/src/email/validation.ts +11 -0
- package/src/encryption/__tests__/calendar-events.test.ts +411 -0
- package/src/encryption/__tests__/configuration.test.ts +114 -0
- package/src/encryption/__tests__/field-encryption.test.ts +232 -0
- package/src/encryption/__tests__/key-generation.test.ts +30 -0
- package/src/encryption/__tests__/performance-edge-cases.test.ts +187 -0
- package/src/encryption/__tests__/test-helpers.ts +22 -0
- package/src/encryption/__tests__/workspace-key-encryption.test.ts +129 -0
- package/src/encryption/encryption-service.ts +343 -0
- package/src/encryption/index.ts +25 -0
- package/src/encryption/types.ts +57 -0
- package/src/exchange-rates.ts +49 -0
- package/src/feature-flags/__tests__/feature-flags.test.ts +302 -0
- package/src/feature-flags/core.ts +322 -0
- package/src/feature-flags/data.ts +16 -0
- package/src/feature-flags/default.ts +18 -0
- package/src/feature-flags/index.ts +7 -0
- package/src/feature-flags/requestable-features.ts +79 -0
- package/src/feature-flags/types.ts +4 -0
- package/src/fetcher.ts +2 -0
- package/src/finance/index.ts +4 -0
- package/src/finance/interest-calculator.ts +456 -0
- package/src/finance/interest-detector.ts +141 -0
- package/src/finance/transform-invoice-results.ts +219 -0
- package/src/finance/wallet-permissions.test.ts +169 -0
- package/src/finance/wallet-permissions.ts +82 -0
- package/src/format.ts +120 -1
- package/src/generated/platform-build-metadata.ts +11 -0
- package/src/hooks/use-platform.ts +64 -0
- package/src/html-sanitizer.ts +155 -0
- package/src/internal-domains.ts +497 -0
- package/src/keyboard-preset.ts +109 -0
- package/src/label-colors.ts +213 -0
- package/src/launchable-apps.test.ts +126 -0
- package/src/launchable-apps.ts +490 -0
- package/src/name-helper.ts +269 -0
- package/src/next-config.test.ts +234 -0
- package/src/next-config.ts +203 -0
- package/src/node-diff.ts +375 -0
- package/src/notification-service.ts +379 -0
- package/src/nova/scores/__tests__/calculate.test.ts +254 -0
- package/src/nova/scores/calculate.ts +132 -0
- package/src/nova/submissions/check-permission.ts +132 -0
- package/src/onboarding-helper.ts +213 -0
- package/src/path-helper.ts +93 -0
- package/src/permissions.tsx +1170 -0
- package/src/plan-helpers.test.ts +188 -0
- package/src/plan-helpers.ts +80 -0
- package/src/platform-release.test.ts +74 -0
- package/src/platform-release.ts +155 -0
- package/src/portless.ts +124 -0
- package/src/priority-styles.ts +42 -0
- package/src/request-emoji-limit.ts +335 -0
- package/src/search-helper.ts +18 -0
- package/src/search.test.ts +89 -0
- package/src/search.ts +355 -0
- package/src/storage-display-name.ts +30 -0
- package/src/storage-path.ts +147 -0
- package/src/tag-utils.ts +159 -0
- package/src/task/reorder.ts +245 -0
- package/src/task/transformers.ts +149 -0
- package/src/task-date-timezone.ts +133 -0
- package/src/task-description-content.ts +240 -0
- package/src/task-helper/board.ts +193 -0
- package/src/task-helper/bulk-actions.ts +564 -0
- package/src/task-helper/personal-external-staging.ts +21 -0
- package/src/task-helper/recycle-bin.ts +202 -0
- package/src/task-helper/relationships.ts +346 -0
- package/src/task-helper/shared.ts +109 -0
- package/src/task-helper/sort-keys.ts +337 -0
- package/src/task-helper/task-hooks-basic.ts +342 -0
- package/src/task-helper/task-hooks-move.ts +264 -0
- package/src/task-helper/task-operations.ts +278 -0
- package/src/task-helper.ts +12 -0
- package/src/task-helpers.ts +241 -0
- package/src/task-list-status.ts +62 -0
- package/src/task-overrides.ts +82 -0
- package/src/task-snapshot.ts +374 -0
- package/src/text-diff.ts +81 -0
- package/src/text-helper.ts +537 -0
- package/src/time-helper.ts +63 -0
- package/src/time-tracker-period.ts +73 -0
- package/src/timeblock-helper.ts +418 -0
- package/src/timezone.ts +190 -0
- package/src/timezones.json +1271 -0
- package/src/upstash-rest.ts +56 -0
- package/src/user-helper.ts +296 -0
- package/src/uuid-helper.ts +11 -0
- package/src/workspace-handle.ts +10 -0
- package/src/workspace-helper.ts +1408 -0
- package/src/workspace-limits.ts +68 -0
- package/src/yjs-helper.ts +217 -0
- package/src/yjs-task-description.ts +81 -0
- package/tsconfig.json +3 -5
- package/tsconfig.typecheck.json +33 -0
- package/vitest.config.ts +36 -0
- package/dist/index.d.ts +0 -8
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -2
- package/dist/index.mjs.map +0 -1
- package/eslint.config.mjs +0 -20
- package/rollup.config.js +0 -41
- 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);
|