@tuturuuu/utils 0.0.2 → 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 +122 -3
- 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,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);
|