@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,155 @@
1
+ /**
2
+ * Simple HTML sanitizer for event descriptions
3
+ * Allows only safe tags and attributes to prevent XSS attacks
4
+ */
5
+
6
+ // Allowed HTML tags for event descriptions
7
+ const ALLOWED_TAGS = [
8
+ 'p',
9
+ 'br',
10
+ 'strong',
11
+ 'b',
12
+ 'em',
13
+ 'i',
14
+ 'u',
15
+ 'a',
16
+ 'ul',
17
+ 'ol',
18
+ 'li',
19
+ 'span',
20
+ 'div',
21
+ ];
22
+
23
+ // Allowed attributes for each tag
24
+ const ALLOWED_ATTRIBUTES: Record<string, string[]> = {
25
+ a: ['href', 'title', 'target', 'rel'],
26
+ span: ['class'],
27
+ div: ['class'],
28
+ };
29
+
30
+ // URL protocols that are safe
31
+ const SAFE_URL_PROTOCOLS = ['http:', 'https:', 'mailto:'];
32
+
33
+ /**
34
+ * Sanitize a URL to prevent javascript: and data: URLs
35
+ */
36
+ function sanitizeUrl(url: string): string | null {
37
+ try {
38
+ const trimmed = url.trim();
39
+
40
+ // Allow relative URLs
41
+ if (
42
+ trimmed.startsWith('/') ||
43
+ trimmed.startsWith('./') ||
44
+ trimmed.startsWith('../')
45
+ ) {
46
+ return trimmed;
47
+ }
48
+
49
+ // Check protocol for absolute URLs
50
+ const urlObj = new URL(trimmed, window.location.origin);
51
+ if (SAFE_URL_PROTOCOLS.includes(urlObj.protocol)) {
52
+ return trimmed;
53
+ }
54
+
55
+ return null;
56
+ } catch {
57
+ // If URL parsing fails, it's probably not a valid URL
58
+ return null;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Sanitize HTML content by stripping dangerous tags and attributes
64
+ * This is a simple implementation - for production, consider using DOMPurify
65
+ */
66
+ export function sanitizeHtml(html: string): string {
67
+ if (!html) return '';
68
+
69
+ // Create a temporary DOM element to parse HTML
70
+ const temp = document.createElement('div');
71
+ temp.innerHTML = html;
72
+
73
+ // Recursive function to sanitize nodes
74
+ function sanitizeNode(node: Node): Node | null {
75
+ // Text nodes are always safe
76
+ if (node.nodeType === Node.TEXT_NODE) {
77
+ return node.cloneNode(true);
78
+ }
79
+
80
+ // Only process element nodes
81
+ if (node.nodeType !== Node.ELEMENT_NODE) {
82
+ return null;
83
+ }
84
+
85
+ const element = node as Element;
86
+ const tagName = element.tagName.toLowerCase();
87
+
88
+ // Check if tag is allowed
89
+ if (!ALLOWED_TAGS.includes(tagName)) {
90
+ // For disallowed tags, keep their text content but not the tag itself
91
+ const textContent = element.textContent || '';
92
+ return document.createTextNode(textContent);
93
+ }
94
+
95
+ // Create a new element with the same tag
96
+ const newElement = document.createElement(tagName);
97
+
98
+ // Copy allowed attributes
99
+ const allowedAttrs = ALLOWED_ATTRIBUTES[tagName] || [];
100
+ for (const attr of allowedAttrs) {
101
+ const value = element.getAttribute(attr);
102
+ if (value) {
103
+ // Special handling for URLs in href
104
+ if (attr === 'href') {
105
+ const sanitizedUrl = sanitizeUrl(value);
106
+ if (sanitizedUrl) {
107
+ newElement.setAttribute(attr, sanitizedUrl);
108
+ // Add rel="noopener noreferrer" for external links for security
109
+ if (sanitizedUrl.startsWith('http')) {
110
+ newElement.setAttribute('rel', 'noopener noreferrer');
111
+ newElement.setAttribute('target', '_blank');
112
+ }
113
+ }
114
+ } else {
115
+ newElement.setAttribute(attr, value);
116
+ }
117
+ }
118
+ }
119
+
120
+ // Recursively sanitize child nodes
121
+ for (const child of Array.from(element.childNodes)) {
122
+ const sanitizedChild = sanitizeNode(child);
123
+ if (sanitizedChild) {
124
+ newElement.appendChild(sanitizedChild);
125
+ }
126
+ }
127
+
128
+ return newElement;
129
+ }
130
+
131
+ // Sanitize all child nodes
132
+ const sanitizedDiv = document.createElement('div');
133
+ for (const child of Array.from(temp.childNodes)) {
134
+ const sanitizedChild = sanitizeNode(child);
135
+ if (sanitizedChild) {
136
+ sanitizedDiv.appendChild(sanitizedChild);
137
+ }
138
+ }
139
+
140
+ return sanitizedDiv.innerHTML;
141
+ }
142
+
143
+ /**
144
+ * Convert plain text to HTML, preserving line breaks
145
+ */
146
+ export function textToHtml(text: string): string {
147
+ return text.replace(/\n/g, '<br>');
148
+ }
149
+
150
+ /**
151
+ * Check if a string contains HTML tags
152
+ */
153
+ export function containsHtml(str: string): boolean {
154
+ return /<[^>]+>/g.test(str);
155
+ }
@@ -0,0 +1,497 @@
1
+ import {
2
+ getTuturuuuPortlessAppOrigin,
3
+ TUTURUUU_PORTLESS_APP_HOSTS,
4
+ } from './portless';
5
+
6
+ export const PRODUCTION_INTERNAL_APP_DOMAINS = [
7
+ {
8
+ name: 'apps',
9
+ url: 'https://apps.tuturuuu.com',
10
+ },
11
+ {
12
+ name: 'platform',
13
+ url: 'https://tuturuuu.com',
14
+ },
15
+ {
16
+ name: 'cms',
17
+ url: 'https://cms.tuturuuu.com',
18
+ },
19
+ {
20
+ name: 'calendar',
21
+ url: 'https://calendar.tuturuuu.com',
22
+ },
23
+ {
24
+ name: 'chat',
25
+ url: 'https://chat.tuturuuu.com',
26
+ },
27
+ {
28
+ name: 'drive',
29
+ url: 'https://drive.tuturuuu.com',
30
+ },
31
+ {
32
+ name: 'mail',
33
+ url: 'https://mail.tuturuuu.com',
34
+ },
35
+ {
36
+ name: 'meet',
37
+ url: 'https://meet.tuturuuu.com',
38
+ },
39
+ {
40
+ name: 'qr',
41
+ url: 'https://qr.tuturuuu.com',
42
+ },
43
+ {
44
+ name: 'nova',
45
+ url: 'https://nova.ai.vn',
46
+ },
47
+ {
48
+ name: 'mira',
49
+ url: 'https://mira.tuturuuu.com',
50
+ },
51
+ {
52
+ name: 'rewise',
53
+ url: 'https://rewise.me',
54
+ },
55
+ {
56
+ name: 'tasks',
57
+ url: 'https://tasks.tuturuuu.com',
58
+ },
59
+ {
60
+ name: 'finance',
61
+ url: 'https://finance.tuturuuu.com',
62
+ },
63
+ {
64
+ name: 'inventory',
65
+ url: 'https://inventory.tuturuuu.com',
66
+ },
67
+ {
68
+ name: 'storefront',
69
+ url: 'https://storefront.tuturuuu.com',
70
+ },
71
+ {
72
+ name: 'track',
73
+ url: 'https://track.tuturuuu.com',
74
+ },
75
+ {
76
+ name: 'learn',
77
+ url: 'https://learn.tuturuuu.com',
78
+ },
79
+ {
80
+ name: 'teach',
81
+ url: 'https://teach.tuturuuu.com',
82
+ },
83
+ {
84
+ name: 'hive',
85
+ url: 'https://hive.tuturuuu.com',
86
+ },
87
+ {
88
+ name: 'mind',
89
+ url: 'https://mind.tuturuuu.com',
90
+ },
91
+ ] as const;
92
+
93
+ export const PORTLESS_INTERNAL_APP_DOMAINS = [
94
+ {
95
+ name: 'apps',
96
+ url: getTuturuuuPortlessAppOrigin('apps'),
97
+ },
98
+ {
99
+ name: 'platform',
100
+ url: getTuturuuuPortlessAppOrigin('platform'),
101
+ },
102
+ {
103
+ name: 'cms',
104
+ url: getTuturuuuPortlessAppOrigin('cms'),
105
+ },
106
+ {
107
+ name: 'calendar',
108
+ url: getTuturuuuPortlessAppOrigin('calendar'),
109
+ },
110
+ {
111
+ name: 'chat',
112
+ url: getTuturuuuPortlessAppOrigin('chat'),
113
+ },
114
+ {
115
+ name: 'drive',
116
+ url: getTuturuuuPortlessAppOrigin('drive'),
117
+ },
118
+ {
119
+ name: 'mail',
120
+ url: getTuturuuuPortlessAppOrigin('mail'),
121
+ },
122
+ {
123
+ name: 'meet',
124
+ url: getTuturuuuPortlessAppOrigin('meet'),
125
+ },
126
+ {
127
+ name: 'qr',
128
+ url: getTuturuuuPortlessAppOrigin('qr'),
129
+ },
130
+ {
131
+ name: 'nova',
132
+ url: getTuturuuuPortlessAppOrigin('nova'),
133
+ },
134
+ {
135
+ name: 'rewise',
136
+ url: getTuturuuuPortlessAppOrigin('rewise'),
137
+ },
138
+ {
139
+ name: 'tasks',
140
+ url: getTuturuuuPortlessAppOrigin('tasks'),
141
+ },
142
+ {
143
+ name: 'finance',
144
+ url: getTuturuuuPortlessAppOrigin('finance'),
145
+ },
146
+ {
147
+ name: 'inventory',
148
+ url: getTuturuuuPortlessAppOrigin('inventory'),
149
+ },
150
+ {
151
+ name: 'storefront',
152
+ url: getTuturuuuPortlessAppOrigin('storefront'),
153
+ },
154
+ {
155
+ name: 'track',
156
+ url: getTuturuuuPortlessAppOrigin('track'),
157
+ },
158
+ {
159
+ name: 'learn',
160
+ url: getTuturuuuPortlessAppOrigin('learn'),
161
+ },
162
+ {
163
+ name: 'teach',
164
+ url: getTuturuuuPortlessAppOrigin('teach'),
165
+ },
166
+ {
167
+ name: 'hive',
168
+ url: getTuturuuuPortlessAppOrigin('hive'),
169
+ },
170
+ {
171
+ name: 'mind',
172
+ url: getTuturuuuPortlessAppOrigin('mind'),
173
+ },
174
+ ] as const;
175
+
176
+ export const LOCALHOST_INTERNAL_APP_DOMAINS = [
177
+ {
178
+ name: 'apps',
179
+ url: 'http://localhost:7818',
180
+ },
181
+ {
182
+ name: 'cms',
183
+ url: 'http://localhost:7811',
184
+ },
185
+ {
186
+ name: 'calendar',
187
+ url: 'http://localhost:7806',
188
+ },
189
+ {
190
+ name: 'chat',
191
+ url: 'http://localhost:7821',
192
+ },
193
+ {
194
+ name: 'drive',
195
+ url: 'http://localhost:7817',
196
+ },
197
+ {
198
+ name: 'mail',
199
+ url: 'http://localhost:7820',
200
+ },
201
+ {
202
+ name: 'meet',
203
+ url: 'http://localhost:7807',
204
+ },
205
+ {
206
+ name: 'qr',
207
+ url: 'http://localhost:7819',
208
+ },
209
+ {
210
+ name: 'platform',
211
+ url: 'http://localhost:7803',
212
+ },
213
+ {
214
+ name: 'rewise',
215
+ url: 'http://localhost:7804',
216
+ },
217
+ {
218
+ name: 'nova',
219
+ url: 'http://localhost:7805',
220
+ },
221
+ {
222
+ name: 'tasks',
223
+ url: 'http://localhost:7809',
224
+ },
225
+ {
226
+ name: 'finance',
227
+ url: 'http://localhost:7808',
228
+ },
229
+ {
230
+ name: 'inventory',
231
+ url: 'http://localhost:7815',
232
+ },
233
+ {
234
+ name: 'storefront',
235
+ url: 'http://localhost:7822',
236
+ },
237
+ {
238
+ name: 'track',
239
+ url: 'http://localhost:7810',
240
+ },
241
+ {
242
+ name: 'learn',
243
+ url: 'http://localhost:7812',
244
+ },
245
+ {
246
+ name: 'teach',
247
+ url: 'http://localhost:7813',
248
+ },
249
+ {
250
+ name: 'hive',
251
+ url: 'http://localhost:7814',
252
+ },
253
+ {
254
+ name: 'mind',
255
+ url: 'http://localhost:7816',
256
+ },
257
+ ] as const;
258
+
259
+ export const DEV_INTERNAL_APP_DOMAINS = [
260
+ ...PORTLESS_INTERNAL_APP_DOMAINS,
261
+ ...LOCALHOST_INTERNAL_APP_DOMAINS,
262
+ ] as const;
263
+
264
+ export const APP_DOMAIN_MAP = [
265
+ ...PRODUCTION_INTERNAL_APP_DOMAINS,
266
+ ...DEV_INTERNAL_APP_DOMAINS,
267
+ ] as const;
268
+
269
+ export type AppName = (typeof APP_DOMAIN_MAP)[number]['name'];
270
+
271
+ export type AppDomain = {
272
+ kind?: 'external' | 'internal';
273
+ name: string;
274
+ url: string;
275
+ };
276
+
277
+ export type AppDomainUrlMatch = AppDomain & {
278
+ canonicalUrl: string;
279
+ };
280
+
281
+ export function getPortlessInternalAppUrl(appName: AppName) {
282
+ return appName in TUTURUUU_PORTLESS_APP_HOSTS
283
+ ? getTuturuuuPortlessAppOrigin(
284
+ appName as keyof typeof TUTURUUU_PORTLESS_APP_HOSTS
285
+ )
286
+ : null;
287
+ }
288
+
289
+ export function getLocalInternalAppUrl(appName: AppName, legacyUrl: string) {
290
+ return getPortlessInternalAppUrl(appName) ?? legacyUrl;
291
+ }
292
+
293
+ function parseExternalAppDomainEntry(entry: string): AppDomain | null {
294
+ const trimmed = entry.trim();
295
+
296
+ if (!trimmed) {
297
+ return null;
298
+ }
299
+
300
+ const separatorIndex = trimmed.indexOf(':');
301
+
302
+ if (separatorIndex <= 0) {
303
+ return null;
304
+ }
305
+
306
+ const name = trimmed.slice(0, separatorIndex).trim().toLowerCase();
307
+ const url = trimmed.slice(separatorIndex + 1).trim();
308
+
309
+ if (!/^[a-z0-9_-]{1,64}$/u.test(name)) {
310
+ return null;
311
+ }
312
+
313
+ try {
314
+ return {
315
+ kind: 'external',
316
+ name,
317
+ url: new URL(url).origin,
318
+ };
319
+ } catch {
320
+ return null;
321
+ }
322
+ }
323
+
324
+ export function getConfiguredExternalAppDomains(): AppDomain[] {
325
+ const configured =
326
+ process.env.NEXT_PUBLIC_TUTURUUU_EXTERNAL_APP_DOMAINS ??
327
+ process.env.TUTURUUU_EXTERNAL_APP_DOMAINS;
328
+
329
+ if (!configured?.trim()) {
330
+ return [];
331
+ }
332
+
333
+ return configured
334
+ .split(/[,\n]/u)
335
+ .map(parseExternalAppDomainEntry)
336
+ .filter((entry): entry is AppDomain => Boolean(entry));
337
+ }
338
+
339
+ export function getAppDomainMap(): AppDomain[] {
340
+ return [
341
+ ...APP_DOMAIN_MAP.map((domain) => ({
342
+ ...domain,
343
+ kind: 'internal' as const,
344
+ })),
345
+ ...getConfiguredExternalAppDomains(),
346
+ ];
347
+ }
348
+
349
+ function parseHttpUrl(value: string) {
350
+ try {
351
+ const url = new URL(value);
352
+
353
+ if (url.protocol !== 'http:' && url.protocol !== 'https:') {
354
+ return null;
355
+ }
356
+
357
+ return url;
358
+ } catch {
359
+ return null;
360
+ }
361
+ }
362
+
363
+ function canUpgradeToRegisteredHttpsOrigin(value: URL, registeredUrl: URL) {
364
+ return (
365
+ registeredUrl.protocol === 'https:' &&
366
+ value.protocol === 'http:' &&
367
+ value.hostname === registeredUrl.hostname &&
368
+ value.port === registeredUrl.port
369
+ );
370
+ }
371
+
372
+ const PORTLESS_APP_HOSTS = new Set(Object.values(TUTURUUU_PORTLESS_APP_HOSTS));
373
+
374
+ function canUseLocalPortlessProxyPort(value: URL, registeredUrl: URL) {
375
+ return (
376
+ value.protocol === registeredUrl.protocol &&
377
+ !registeredUrl.port &&
378
+ Boolean(value.port) &&
379
+ PORTLESS_APP_HOSTS.has(registeredUrl.hostname)
380
+ );
381
+ }
382
+
383
+ function hasCompatiblePortlessPort(value: URL, registeredUrl: URL) {
384
+ return (
385
+ value.port === registeredUrl.port ||
386
+ canUseLocalPortlessProxyPort(value, registeredUrl)
387
+ );
388
+ }
389
+
390
+ function matchesPortlessProxyOrigin(value: URL, registeredUrl: URL) {
391
+ return (
392
+ canUseLocalPortlessProxyPort(value, registeredUrl) &&
393
+ value.hostname === registeredUrl.hostname
394
+ );
395
+ }
396
+
397
+ function matchesPrefixedPortlessOrigin(value: URL, registeredUrl: URL) {
398
+ if (
399
+ value.protocol !== registeredUrl.protocol ||
400
+ !hasCompatiblePortlessPort(value, registeredUrl) ||
401
+ !PORTLESS_APP_HOSTS.has(registeredUrl.hostname) ||
402
+ PORTLESS_APP_HOSTS.has(value.hostname)
403
+ ) {
404
+ return false;
405
+ }
406
+
407
+ const suffix = `.${registeredUrl.hostname}`;
408
+
409
+ if (!value.hostname.endsWith(suffix)) {
410
+ return false;
411
+ }
412
+
413
+ const prefix = value.hostname.slice(0, -suffix.length);
414
+
415
+ return Boolean(prefix) && !prefix.includes('.');
416
+ }
417
+
418
+ function serializeUrl(value: URL) {
419
+ return value.pathname === '/' && !value.search && !value.hash
420
+ ? value.origin
421
+ : value.toString();
422
+ }
423
+
424
+ function matchesAppDomainUrl(value: URL, domain: AppDomain) {
425
+ const registeredUrl = parseHttpUrl(domain.url);
426
+
427
+ if (!registeredUrl) {
428
+ return false;
429
+ }
430
+
431
+ if (value.origin === registeredUrl.origin) {
432
+ return true;
433
+ }
434
+
435
+ if (
436
+ domain.kind === 'internal' &&
437
+ (matchesPortlessProxyOrigin(value, registeredUrl) ||
438
+ matchesPrefixedPortlessOrigin(value, registeredUrl))
439
+ ) {
440
+ return true;
441
+ }
442
+
443
+ return domain.kind === 'internal'
444
+ ? canUpgradeToRegisteredHttpsOrigin(value, registeredUrl)
445
+ : false;
446
+ }
447
+
448
+ function canonicalizeAppDomainUrl(value: URL, domain: AppDomain) {
449
+ const registeredUrl = parseHttpUrl(domain.url);
450
+
451
+ if (
452
+ registeredUrl &&
453
+ domain.kind === 'internal' &&
454
+ (canUpgradeToRegisteredHttpsOrigin(value, registeredUrl) ||
455
+ matchesPortlessProxyOrigin(value, registeredUrl))
456
+ ) {
457
+ const canonicalUrl = new URL(value.toString());
458
+ canonicalUrl.protocol = registeredUrl.protocol;
459
+ canonicalUrl.hostname = registeredUrl.hostname;
460
+ canonicalUrl.port = registeredUrl.port;
461
+ return serializeUrl(canonicalUrl);
462
+ }
463
+
464
+ return serializeUrl(value);
465
+ }
466
+
467
+ export function getAppDomainByUrl(value: string): AppDomainUrlMatch | null {
468
+ const url = parseHttpUrl(value);
469
+
470
+ if (!url) {
471
+ return null;
472
+ }
473
+
474
+ const domain = getAppDomainMap().find((entry) =>
475
+ matchesAppDomainUrl(url, entry)
476
+ );
477
+
478
+ return domain
479
+ ? {
480
+ ...domain,
481
+ canonicalUrl: canonicalizeAppDomainUrl(url, domain),
482
+ }
483
+ : null;
484
+ }
485
+
486
+ export function getInternalAppDomainByUrl(
487
+ value: string
488
+ ): AppDomainUrlMatch | null {
489
+ const appDomain = getAppDomainByUrl(value);
490
+
491
+ return appDomain?.kind === 'internal' ? appDomain : null;
492
+ }
493
+
494
+ export const INTERNAL_DOMAINS = [
495
+ ...PRODUCTION_INTERNAL_APP_DOMAINS,
496
+ ...DEV_INTERNAL_APP_DOMAINS,
497
+ ].map((domain) => domain.url);