@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,91 @@
1
+ import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
2
+ import dayjs from 'dayjs';
3
+ import timezone from 'dayjs/plugin/timezone';
4
+ import utc from 'dayjs/plugin/utc';
5
+
6
+ dayjs.extend(timezone);
7
+ dayjs.extend(utc);
8
+
9
+ export function isAllDayEvent(
10
+ event: Pick<CalendarEvent, 'start_at' | 'end_at'>
11
+ ): boolean {
12
+ const start = dayjs(event.start_at);
13
+ const end = dayjs(event.end_at);
14
+
15
+ const durationMs = end.diff(start, 'millisecond');
16
+ const isMultipleOf24Hours = durationMs % (24 * 60 * 60 * 1000) === 0;
17
+
18
+ return durationMs > 0 && isMultipleOf24Hours;
19
+ }
20
+
21
+ // Helper function to convert Google Calendar all-day events to proper timezone
22
+ export function convertGoogleAllDayEvent(
23
+ startDate: string | undefined,
24
+ endDate: string | undefined,
25
+ userTimezone?: string
26
+ ): { start_at: string; end_at: string } {
27
+ // Check if this is a date-only format (all-day event from Google)
28
+ const isDateOnly = (dateStr: string) => /^\d{4}-\d{2}-\d{2}$/.test(dateStr);
29
+
30
+ if (!startDate || !endDate) {
31
+ const now = dayjs();
32
+ return {
33
+ start_at: now.toISOString(),
34
+ end_at: now.add(1, 'hour').toISOString(),
35
+ };
36
+ }
37
+
38
+ // If both are date-only (Google all-day event), convert to user's timezone midnight
39
+ if (isDateOnly(startDate) && isDateOnly(endDate)) {
40
+ const tz =
41
+ userTimezone === 'auto'
42
+ ? typeof window !== 'undefined'
43
+ ? Intl.DateTimeFormat().resolvedOptions().timeZone
44
+ : undefined
45
+ : userTimezone;
46
+
47
+ const startAtMidnight = tz
48
+ ? dayjs.tz(`${startDate}T00:00:00`, tz)
49
+ : dayjs(`${startDate}T00:00:00`);
50
+
51
+ const endAtMidnight = tz
52
+ ? dayjs.tz(`${endDate}T00:00:00`, tz)
53
+ : dayjs(`${endDate}T00:00:00`);
54
+
55
+ return {
56
+ start_at: startAtMidnight.toISOString(),
57
+ end_at: endAtMidnight.toISOString(),
58
+ };
59
+ }
60
+
61
+ // Otherwise, use the dates as-is (they're already dateTime format)
62
+ return {
63
+ start_at: startDate,
64
+ end_at: endDate,
65
+ };
66
+ }
67
+
68
+ // Helper to create an all-day event in user's timezone
69
+ export function createAllDayEvent(
70
+ date: Date,
71
+ userTimezone?: string,
72
+ durationDays: number = 1
73
+ ): { start_at: string; end_at: string } {
74
+ const tz =
75
+ userTimezone === 'auto'
76
+ ? typeof window !== 'undefined'
77
+ ? Intl.DateTimeFormat().resolvedOptions().timeZone
78
+ : undefined
79
+ : userTimezone;
80
+
81
+ const startAtMidnight = tz
82
+ ? dayjs.tz(date, tz).startOf('day')
83
+ : dayjs(date).startOf('day');
84
+
85
+ const endAtMidnight = startAtMidnight.add(durationDays, 'day');
86
+
87
+ return {
88
+ start_at: startAtMidnight.toISOString(),
89
+ end_at: endAtMidnight.toISOString(),
90
+ };
91
+ }
@@ -0,0 +1,110 @@
1
+ export const getEventStyles = (
2
+ color: string
3
+ ): {
4
+ bg: string;
5
+ border: string;
6
+ text: string;
7
+ dragBg: string;
8
+ syncingBg: string;
9
+ successBg: string;
10
+ errorBg: string;
11
+ } => {
12
+ const colorStyles = {
13
+ BLUE: {
14
+ bg: 'bg-calendar-bg-blue hover:ring-dynamic-light-blue/80',
15
+ border: 'border-dynamic-light-blue/80',
16
+ text: 'text-dynamic-light-blue',
17
+ dragBg: 'bg-calendar-bg-blue',
18
+ syncingBg: 'bg-calendar-bg-blue',
19
+ successBg: 'bg-calendar-bg-blue',
20
+ errorBg: 'bg-calendar-bg-red',
21
+ },
22
+ RED: {
23
+ bg: 'bg-calendar-bg-red hover:ring-dynamic-light-red/80',
24
+ border: 'border-dynamic-light-red/80',
25
+ text: 'text-dynamic-light-red',
26
+ dragBg: 'bg-calendar-bg-red',
27
+ syncingBg: 'bg-calendar-bg-red',
28
+ successBg: 'bg-calendar-bg-red',
29
+ errorBg: 'bg-calendar-bg-red',
30
+ },
31
+ GREEN: {
32
+ bg: 'bg-calendar-bg-green hover:ring-dynamic-light-green/80',
33
+ border: 'border-dynamic-light-green/80',
34
+ text: 'text-dynamic-light-green',
35
+ dragBg: 'bg-calendar-bg-green',
36
+ syncingBg: 'bg-calendar-bg-green',
37
+ successBg: 'bg-calendar-bg-green',
38
+ errorBg: 'bg-calendar-bg-red',
39
+ },
40
+ YELLOW: {
41
+ bg: 'bg-calendar-bg-yellow hover:ring-dynamic-light-yellow/80',
42
+ border: 'border-dynamic-light-yellow/80',
43
+ text: 'text-dynamic-light-yellow',
44
+ dragBg: 'bg-calendar-bg-yellow',
45
+ syncingBg: 'bg-calendar-bg-yellow',
46
+ successBg: 'bg-calendar-bg-yellow',
47
+ errorBg: 'bg-calendar-bg-red',
48
+ },
49
+ PURPLE: {
50
+ bg: 'bg-calendar-bg-purple hover:ring-dynamic-light-purple/80',
51
+ border: 'border-dynamic-light-purple/80',
52
+ text: 'text-dynamic-light-purple',
53
+ dragBg: 'bg-calendar-bg-purple',
54
+ syncingBg: 'bg-calendar-bg-purple',
55
+ successBg: 'bg-calendar-bg-purple',
56
+ errorBg: 'bg-calendar-bg-red',
57
+ },
58
+ PINK: {
59
+ bg: 'bg-calendar-bg-pink hover:ring-dynamic-light-pink/80',
60
+ border: 'border-dynamic-light-pink/80',
61
+ text: 'text-dynamic-light-pink',
62
+ dragBg: 'bg-calendar-bg-pink',
63
+ syncingBg: 'bg-calendar-bg-pink',
64
+ successBg: 'bg-calendar-bg-pink',
65
+ errorBg: 'bg-calendar-bg-red',
66
+ },
67
+ ORANGE: {
68
+ bg: 'bg-calendar-bg-orange hover:ring-dynamic-light-orange/80',
69
+ border: 'border-dynamic-light-orange/80',
70
+ text: 'text-dynamic-light-orange',
71
+ dragBg: 'bg-calendar-bg-orange',
72
+ syncingBg: 'bg-calendar-bg-orange',
73
+ successBg: 'bg-calendar-bg-orange',
74
+ errorBg: 'bg-calendar-bg-red',
75
+ },
76
+ INDIGO: {
77
+ bg: 'bg-calendar-bg-indigo hover:ring-dynamic-light-indigo/80',
78
+ border: 'border-dynamic-light-indigo/80',
79
+ text: 'text-dynamic-light-indigo',
80
+ dragBg: 'bg-calendar-bg-indigo',
81
+ syncingBg: 'bg-calendar-bg-indigo',
82
+ successBg: 'bg-calendar-bg-indigo',
83
+ errorBg: 'bg-calendar-bg-red',
84
+ },
85
+ CYAN: {
86
+ bg: 'bg-calendar-bg-cyan hover:ring-dynamic-light-cyan/80',
87
+ border: 'border-dynamic-light-cyan/80',
88
+ text: 'text-dynamic-light-cyan',
89
+ dragBg: 'bg-calendar-bg-cyan',
90
+ syncingBg: 'bg-calendar-bg-cyan',
91
+ successBg: 'bg-calendar-bg-cyan',
92
+ errorBg: 'bg-calendar-bg-red',
93
+ },
94
+ GRAY: {
95
+ bg: 'bg-calendar-bg-gray hover:ring-dynamic-light-gray/80',
96
+ border: 'border-dynamic-light-gray/80',
97
+ text: 'text-dynamic-light-gray',
98
+ dragBg: 'bg-calendar-bg-gray',
99
+ syncingBg: 'bg-calendar-bg-gray',
100
+ successBg: 'bg-calendar-bg-gray',
101
+ errorBg: 'bg-calendar-bg-red',
102
+ },
103
+ } as const;
104
+
105
+ const normalizedColor = color?.toUpperCase() ?? 'BLUE';
106
+ const colorStyle = colorStyles[normalizedColor as keyof typeof colorStyles];
107
+
108
+ if (!colorStyle) return colorStyles.BLUE;
109
+ return colorStyle;
110
+ };
@@ -0,0 +1,99 @@
1
+ import type { Metadata, Viewport } from 'next';
2
+ import { Noto_Sans } from 'next/font/google';
3
+ import { DEV_MODE } from '../constants';
4
+
5
+ export const font = Noto_Sans({
6
+ subsets: ['latin', 'vietnamese'],
7
+ display: 'block',
8
+ });
9
+
10
+ interface MetadataProps {
11
+ config: {
12
+ name: string;
13
+ url: string;
14
+ ogImage: string;
15
+ keywords?: string[];
16
+ description: {
17
+ en: string;
18
+ vi?: string;
19
+ };
20
+ };
21
+ params: Promise<{
22
+ locale: string;
23
+ }>;
24
+ }
25
+
26
+ export async function generateCommonMetadata({
27
+ config,
28
+ params,
29
+ }: MetadataProps): Promise<Metadata> {
30
+ const { locale } = await params;
31
+ const description =
32
+ locale === 'vi' ? config.description.vi : config.description.en;
33
+
34
+ return {
35
+ applicationName: config.name,
36
+ title: {
37
+ default: config.name,
38
+ template: `${DEV_MODE ? '[DEV] ' : ''} %s | ${config.name}`,
39
+ },
40
+ metadataBase: new URL(config.url),
41
+ description,
42
+ keywords: config.keywords ?? [
43
+ 'React.js',
44
+ 'Next.js',
45
+ 'Tailwind CSS',
46
+ 'TypeScript',
47
+ 'Biome',
48
+ ],
49
+ appleWebApp: {
50
+ capable: true,
51
+ statusBarStyle: 'default',
52
+ title: config.name,
53
+ },
54
+ formatDetection: {
55
+ telephone: false,
56
+ },
57
+ openGraph: {
58
+ type: 'website',
59
+ locale,
60
+ url: config.url,
61
+ title: config.name,
62
+ description,
63
+ siteName: config.name,
64
+ images: [
65
+ {
66
+ url: config.ogImage,
67
+ width: 1200,
68
+ height: 630,
69
+ alt: config.name,
70
+ },
71
+ ],
72
+ },
73
+ twitter: {
74
+ card: 'summary_large_image',
75
+ title: config.name,
76
+ description,
77
+ images: [config.ogImage],
78
+ creator: '@tuturuuu',
79
+ },
80
+ icons: {
81
+ icon: '/favicon.ico',
82
+ shortcut: '/favicon-16x16.png',
83
+ apple: '/apple-touch-icon.png',
84
+ },
85
+ manifest: '/manifest.webmanifest',
86
+ };
87
+ }
88
+
89
+ export const viewport: Viewport = {
90
+ width: 'device-width',
91
+ initialScale: 1,
92
+ maximumScale: 5,
93
+ viewportFit: 'cover',
94
+ themeColor: [
95
+ { media: '(prefers-color-scheme: dark)', color: 'black' },
96
+ { media: '(prefers-color-scheme: light)', color: 'white' },
97
+ ],
98
+ colorScheme: 'dark light',
99
+ };
@@ -0,0 +1,15 @@
1
+ import { DEV_MODE } from '../constants';
2
+
3
+ const enabled = false;
4
+
5
+ export const ReactScan = () => {
6
+ if (!DEV_MODE || !enabled) return null;
7
+ return (
8
+ <head>
9
+ <script
10
+ crossOrigin="anonymous"
11
+ src="//unpkg.com/react-scan/dist/auto.global.js"
12
+ />
13
+ </head>
14
+ );
15
+ };
@@ -0,0 +1,160 @@
1
+ import type { WorkspaceConfig } from '@tuturuuu/types/primitives/WorkspaceConfig';
2
+
3
+ // Report template configs
4
+ export const reportConfigs: (WorkspaceConfig & {
5
+ defaultValue: string;
6
+ })[] = [
7
+ {
8
+ id: 'BRAND_LOGO_URL',
9
+ type: 'URL',
10
+ defaultValue: '',
11
+ },
12
+ {
13
+ id: 'BRAND_NAME',
14
+ type: 'TEXT',
15
+ defaultValue: '',
16
+ },
17
+ {
18
+ id: 'BRAND_LOCATION',
19
+ type: 'TEXT',
20
+ defaultValue: '',
21
+ },
22
+ {
23
+ id: 'BRAND_PHONE_NUMBER',
24
+ type: 'TEXT',
25
+ defaultValue: '',
26
+ },
27
+ {
28
+ id: 'REPORT_TITLE_PREFIX',
29
+ type: 'TEXT',
30
+ defaultValue: '',
31
+ },
32
+ {
33
+ id: 'REPORT_TITLE_SUFFIX',
34
+ type: 'TEXT',
35
+ defaultValue: '',
36
+ },
37
+ {
38
+ id: 'REPORT_DEFAULT_TITLE',
39
+ type: 'TEXT',
40
+ defaultValue: '',
41
+ },
42
+ {
43
+ id: 'REPORT_INTRO',
44
+ type: 'TEXT',
45
+ defaultValue: '',
46
+ },
47
+ {
48
+ id: 'REPORT_CONTENT_TEXT',
49
+ type: 'TEXT',
50
+ defaultValue: '',
51
+ },
52
+ {
53
+ id: 'REPORT_SCORE_TEXT',
54
+ type: 'TEXT',
55
+ defaultValue: '',
56
+ },
57
+ {
58
+ id: 'REPORT_FEEDBACK_TEXT',
59
+ type: 'TEXT',
60
+ defaultValue: '',
61
+ },
62
+ {
63
+ id: 'REPORT_CONCLUSION',
64
+ type: 'TEXT',
65
+ defaultValue: '',
66
+ },
67
+ {
68
+ id: 'REPORT_CLOSING',
69
+ type: 'TEXT',
70
+ defaultValue: '',
71
+ },
72
+ {
73
+ id: 'ENABLE_REPORT_EXPORT_ONLY_APPROVED',
74
+ type: 'TEXT',
75
+ defaultValue: 'false',
76
+ },
77
+ {
78
+ id: 'ENABLE_REPORT_PENDING_WATERMARK',
79
+ type: 'TEXT',
80
+ defaultValue: 'false',
81
+ },
82
+ ];
83
+
84
+ // Lead generation email template configs
85
+ export const leadGenerationConfigs: (WorkspaceConfig & {
86
+ defaultValue: string;
87
+ })[] = [
88
+ // Shared brand configs (already in report template)
89
+ {
90
+ id: 'BRAND_LOGO_URL',
91
+ type: 'URL',
92
+ defaultValue: '',
93
+ },
94
+ {
95
+ id: 'BRAND_NAME',
96
+ type: 'TEXT',
97
+ defaultValue: '',
98
+ },
99
+ {
100
+ id: 'BRAND_LOCATION',
101
+ type: 'TEXT',
102
+ defaultValue: '',
103
+ },
104
+ {
105
+ id: 'BRAND_PHONE_NUMBER',
106
+ type: 'TEXT',
107
+ defaultValue: '',
108
+ },
109
+ // Lead generation specific configs
110
+ {
111
+ id: 'LEAD_EMAIL_TITLE',
112
+ type: 'TEXT',
113
+ defaultValue: '',
114
+ },
115
+ {
116
+ id: 'LEAD_EMAIL_GREETING',
117
+ type: 'TEXT',
118
+ defaultValue: '',
119
+ },
120
+ {
121
+ id: 'LEAD_EMAIL_TABLE_HEADER_COMMENTS',
122
+ type: 'TEXT',
123
+ defaultValue: '',
124
+ },
125
+ {
126
+ id: 'LEAD_EMAIL_TABLE_HEADER_SCORE',
127
+ type: 'TEXT',
128
+ defaultValue: '',
129
+ },
130
+ {
131
+ id: 'LEAD_EMAIL_TABLE_SCORE_SCALE',
132
+ type: 'TEXT',
133
+ defaultValue: '',
134
+ },
135
+ {
136
+ id: 'LEAD_EMAIL_FOOTER',
137
+ type: 'TEXT',
138
+ defaultValue: '',
139
+ },
140
+ {
141
+ id: 'LEAD_EMAIL_SIGNATURE_TITLE',
142
+ type: 'TEXT',
143
+ defaultValue: '',
144
+ },
145
+ {
146
+ id: 'LEAD_EMAIL_SIGNATURE_NAME',
147
+ type: 'TEXT',
148
+ defaultValue: '',
149
+ },
150
+ ];
151
+
152
+ // Combined list for backward compatibility
153
+ export const availableConfigs: (WorkspaceConfig & {
154
+ defaultValue: string;
155
+ })[] = [
156
+ ...reportConfigs,
157
+ ...leadGenerationConfigs.filter(
158
+ (config) => !reportConfigs.some((rc) => rc.id === config.id)
159
+ ),
160
+ ];
@@ -0,0 +1,85 @@
1
+ export const GITHUB_OWNER = 'tutur3u';
2
+ export const GITHUB_REPO = 'platform';
3
+ export const ROOT_WORKSPACE_ID = '00000000-0000-0000-0000-000000000000';
4
+ export const INTERNAL_WORKSPACE_SLUG = 'internal';
5
+ export const PERSONAL_WORKSPACE_SLUG = 'personal';
6
+
7
+ // Workspace creation limits
8
+ export const MAX_WORKSPACES_FOR_FREE_USERS = 10;
9
+
10
+ export const resolveWorkspaceId = (identifier: string): string => {
11
+ if (!identifier) return identifier;
12
+
13
+ if (identifier.toLowerCase() === INTERNAL_WORKSPACE_SLUG) {
14
+ return ROOT_WORKSPACE_ID;
15
+ }
16
+
17
+ return identifier;
18
+ };
19
+
20
+ export const normalizeWorkspaceContextId = (identifier?: string | null) => {
21
+ const trimmed = identifier?.trim();
22
+ if (!trimmed) return PERSONAL_WORKSPACE_SLUG;
23
+
24
+ if (trimmed.toLowerCase() === PERSONAL_WORKSPACE_SLUG) {
25
+ return PERSONAL_WORKSPACE_SLUG;
26
+ }
27
+
28
+ return resolveWorkspaceId(trimmed);
29
+ };
30
+
31
+ export const toWorkspaceSlug = (
32
+ workspaceId: string,
33
+ { personal = false }: { personal?: boolean } = {}
34
+ ): string => {
35
+ if (personal) return PERSONAL_WORKSPACE_SLUG;
36
+ if (workspaceId === ROOT_WORKSPACE_ID) return INTERNAL_WORKSPACE_SLUG;
37
+ return workspaceId;
38
+ };
39
+
40
+ export const isInternalWorkspaceSlug = (
41
+ identifier?: string | null
42
+ ): boolean => {
43
+ if (!identifier) return false;
44
+ return identifier.toLowerCase() === INTERNAL_WORKSPACE_SLUG;
45
+ };
46
+
47
+ export const DEV_MODE = process.env.NODE_ENV === 'development';
48
+ export const PROD_MODE = process.env.NODE_ENV === 'production';
49
+
50
+ // ── Generic text field length tiers ──────────────────────────────────────────
51
+ // Use these when a domain-specific constant below does not exist.
52
+ export const MAX_CODE_LENGTH = 10; // locale codes, currency codes
53
+ export const MAX_OTP_LENGTH = 20; // OTP / verification codes
54
+ export const MAX_IP_LENGTH = 45; // IPv6 addresses
55
+ export const MAX_COLOR_LENGTH = 50; // CSS color values, hex strings
56
+ export const MAX_DATE_STRING_LENGTH = 50; // ISO date strings
57
+ export const MAX_SHORT_TEXT_LENGTH = 100; // status, type, timezone, provider
58
+ export const MAX_ID_LENGTH = 255; // non-UUID identifiers, slugs
59
+ export const MAX_NAME_LENGTH = 255; // generic names, titles, labels
60
+ export const MAX_SEARCH_LENGTH = 500; // search queries, reasons, subjects
61
+ export const MAX_MEDIUM_TEXT_LENGTH = 1000; // notes, paths, config values
62
+ export const MAX_URL_LENGTH = 2000; // URLs, tokens, long identifiers
63
+ export const MAX_LONG_TEXT_LENGTH = 10000; // descriptions, content, messages
64
+ export const MAX_RICH_TEXT_LENGTH = 50000; // HTML content, rich text
65
+
66
+ // ── Domain-specific field limits ─────────────────────────────────────────────
67
+ export const MAX_DISPLAY_NAME_LENGTH = 100;
68
+ export const MAX_FULL_NAME_LENGTH = 100;
69
+ export const MAX_BIO_LENGTH = 1000;
70
+ export const MAX_EMAIL_LENGTH = 320; // Based on RFC 5321 (64 for local-part + 1 for @ + 255 for domain)
71
+
72
+ // Payload limits
73
+ export const MAX_PAYLOAD_SIZE = 512 * 1024; // 512KB
74
+ export const MAX_REQUEST_BODY_BYTES = 512 * 1024; // 512KB — global limit for non-file-upload API routes
75
+ export const MAX_TEXT_FIELD_BYTES = 64000;
76
+ // 40KB — covers 10K emoji chars (4 bytes each)
77
+ export const MAX_WORKSPACE_NAME_LENGTH = 100;
78
+ export const MAX_TASK_NAME_LENGTH = 1024;
79
+ export const MAX_TASK_DESCRIPTION_LENGTH = 100000;
80
+ export const MAX_CHAT_MESSAGE_LENGTH = 10000;
81
+ export const MAX_SUPPORT_INQUIRY_LENGTH = 5000;
82
+ export const MAX_CALENDAR_EVENT_TITLE_LENGTH = 255;
83
+ export const MAX_CALENDAR_EVENT_DESCRIPTION_LENGTH = 10000;
84
+ export const MAX_NOTE_TITLE_LENGTH = 255;
85
+ export const MAX_NOTE_CONTENT_LENGTH = 50000;
package/src/crypto.ts ADDED
@@ -0,0 +1,21 @@
1
+ import crypto from 'node:crypto';
2
+
3
+ export function generateSalt() {
4
+ return crypto.randomBytes(10).toString('hex');
5
+ }
6
+
7
+ export async function hashPassword(password: string, salt: string) {
8
+ // concatenate the password and salt
9
+ const passwordWithSalt = password + salt;
10
+
11
+ // use native crypto to hash the password
12
+ const hashedPassword = await crypto.subtle.digest(
13
+ 'SHA-256',
14
+ new TextEncoder().encode(passwordWithSalt)
15
+ );
16
+
17
+ // convert the hashed password to a hex string
18
+ return Array.from(new Uint8Array(hashedPassword))
19
+ .map((b) => b.toString(16).padStart(2, '0'))
20
+ .join('');
21
+ }
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Centralized currency configuration
3
+ *
4
+ * This module provides a single source of truth for all supported currencies.
5
+ * The `SupportedCurrency` type is automatically derived from the array,
6
+ * ensuring type safety without manual maintenance of union types.
7
+ *
8
+ * To add a new currency:
9
+ * 1. Add an entry to SUPPORTED_CURRENCIES below
10
+ * 2. Add translations in apps/web/messages/{en,vi}.json under ws-finance-settings
11
+ */
12
+
13
+ /**
14
+ * All supported currencies with their locale mappings.
15
+ * Sorted alphabetically by currency code.
16
+ */
17
+ export const SUPPORTED_CURRENCIES = [
18
+ { code: 'AED', locale: 'ar-AE', name: 'UAE Dirham' },
19
+ { code: 'AUD', locale: 'en-AU', name: 'Australian Dollar' },
20
+ { code: 'BRL', locale: 'pt-BR', name: 'Brazilian Real' },
21
+ { code: 'CAD', locale: 'en-CA', name: 'Canadian Dollar' },
22
+ { code: 'CHF', locale: 'de-CH', name: 'Swiss Franc' },
23
+ { code: 'CNY', locale: 'zh-CN', name: 'Chinese Yuan' },
24
+ { code: 'CZK', locale: 'cs-CZ', name: 'Czech Koruna' },
25
+ { code: 'DKK', locale: 'da-DK', name: 'Danish Krone' },
26
+ { code: 'EUR', locale: 'de-DE', name: 'Euro' },
27
+ { code: 'GBP', locale: 'en-GB', name: 'British Pound' },
28
+ { code: 'HKD', locale: 'zh-HK', name: 'Hong Kong Dollar' },
29
+ { code: 'HUF', locale: 'hu-HU', name: 'Hungarian Forint' },
30
+ { code: 'IDR', locale: 'id-ID', name: 'Indonesian Rupiah' },
31
+ { code: 'INR', locale: 'hi-IN', name: 'Indian Rupee' },
32
+ { code: 'JPY', locale: 'ja-JP', name: 'Japanese Yen' },
33
+ { code: 'KRW', locale: 'ko-KR', name: 'South Korean Won' },
34
+ { code: 'MXN', locale: 'es-MX', name: 'Mexican Peso' },
35
+ { code: 'MYR', locale: 'ms-MY', name: 'Malaysian Ringgit' },
36
+ { code: 'NOK', locale: 'nb-NO', name: 'Norwegian Krone' },
37
+ { code: 'NZD', locale: 'en-NZ', name: 'New Zealand Dollar' },
38
+ { code: 'PHP', locale: 'en-PH', name: 'Philippine Peso' },
39
+ { code: 'PLN', locale: 'pl-PL', name: 'Polish Zloty' },
40
+ { code: 'SAR', locale: 'ar-SA', name: 'Saudi Riyal' },
41
+ { code: 'SEK', locale: 'sv-SE', name: 'Swedish Krona' },
42
+ { code: 'SGD', locale: 'en-SG', name: 'Singapore Dollar' },
43
+ { code: 'THB', locale: 'th-TH', name: 'Thai Baht' },
44
+ { code: 'TWD', locale: 'zh-TW', name: 'Taiwan Dollar' },
45
+ { code: 'USD', locale: 'en-US', name: 'US Dollar' },
46
+ { code: 'VND', locale: 'vi-VN', name: 'Vietnamese Dong' },
47
+ { code: 'ZAR', locale: 'en-ZA', name: 'South African Rand' },
48
+ ] as const;
49
+
50
+ /**
51
+ * Type representing all supported currency codes.
52
+ * Automatically derived from SUPPORTED_CURRENCIES array.
53
+ */
54
+ export type SupportedCurrency = (typeof SUPPORTED_CURRENCIES)[number]['code'];
55
+
56
+ /**
57
+ * Type for a single currency configuration entry.
58
+ */
59
+ export type CurrencyConfig = (typeof SUPPORTED_CURRENCIES)[number];
60
+
61
+ /**
62
+ * Get the locale for a specific currency.
63
+ * Falls back to 'en-US' for unknown currencies.
64
+ *
65
+ * @param currency - The currency code (e.g., 'USD', 'EUR', 'VND')
66
+ * @returns The BCP 47 locale string for the currency
67
+ */
68
+ export function getCurrencyLocale(currency = 'VND'): string {
69
+ const found = SUPPORTED_CURRENCIES.find(
70
+ (c) => c.code === currency.toUpperCase()
71
+ );
72
+ return found?.locale ?? 'en-US';
73
+ }
74
+
75
+ /**
76
+ * Check if a currency code is supported.
77
+ *
78
+ * @param currency - The currency code to check
79
+ * @returns True if the currency is supported
80
+ */
81
+ export function isSupportedCurrency(
82
+ currency: string
83
+ ): currency is SupportedCurrency {
84
+ return SUPPORTED_CURRENCIES.some((c) => c.code === currency.toUpperCase());
85
+ }
86
+
87
+ /**
88
+ * Get the full configuration for a currency.
89
+ *
90
+ * @param currency - The currency code
91
+ * @returns The currency configuration or undefined if not found
92
+ */
93
+ export function getCurrencyConfig(
94
+ currency: string
95
+ ): CurrencyConfig | undefined {
96
+ return SUPPORTED_CURRENCIES.find((c) => c.code === currency.toUpperCase());
97
+ }