@tuturuuu/utils 0.12.1 → 0.15.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 +69 -0
- package/biome.json +1 -1
- package/package.json +10 -10
- package/src/__tests__/api-proxy-guard.test.ts +40 -17
- package/src/__tests__/app-url.test.ts +6 -0
- package/src/__tests__/task-transformers.test.ts +21 -0
- package/src/abuse-protection/__tests__/index-fail-open.test.ts +93 -0
- package/src/abuse-protection/__tests__/index.test.ts +76 -1
- package/src/abuse-protection/index.ts +136 -123
- package/src/api-proxy-guard.ts +15 -117
- package/src/calendar-settings-resolver.ts +200 -0
- package/src/internal-domains.ts +44 -0
- package/src/launchable-apps.test.ts +44 -0
- package/src/launchable-apps.ts +60 -47
- package/src/platform-release.test.ts +2 -2
- package/src/platform-release.ts +3 -1
- package/src/portless.ts +4 -0
- package/src/shared-cookie.test.ts +114 -0
- package/src/shared-cookie.ts +235 -0
- package/src/task/transformers.ts +2 -0
- package/src/topic-announcements.ts +1 -0
- package/src/user-helper.ts +6 -95
- package/src/workspace-user-link.test.ts +53 -0
- package/src/workspace-user-link.ts +122 -0
- package/src/yjs-helper.ts +2 -2
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calendar Settings Resolver
|
|
3
|
+
*
|
|
4
|
+
* Implements priority system for calendar preferences:
|
|
5
|
+
* User settings > Workspace settings > Auto-detection
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type FirstDayOfWeek = 'auto' | 'sunday' | 'monday' | 'saturday';
|
|
9
|
+
|
|
10
|
+
interface CalendarSettings {
|
|
11
|
+
timezone: string;
|
|
12
|
+
firstDayOfWeek: FirstDayOfWeek;
|
|
13
|
+
timeFormat: '12h' | '24h';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CalendarUserSettings {
|
|
17
|
+
timezone?: string | null;
|
|
18
|
+
first_day_of_week?: string | null;
|
|
19
|
+
time_format?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CalendarWorkspaceSettings {
|
|
23
|
+
timezone?: string | null;
|
|
24
|
+
first_day_of_week?: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Detects the system timezone using Intl API
|
|
29
|
+
*/
|
|
30
|
+
export function detectSystemTimezone(): string {
|
|
31
|
+
try {
|
|
32
|
+
return Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
33
|
+
} catch {
|
|
34
|
+
// Fallback to UTC if detection fails
|
|
35
|
+
return 'UTC';
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Detects the first day of week based on locale
|
|
41
|
+
* Vietnamese: Monday
|
|
42
|
+
* US/Canada: Sunday
|
|
43
|
+
* Middle East: Saturday
|
|
44
|
+
*/
|
|
45
|
+
export function detectLocaleFirstDay(locale?: string): FirstDayOfWeek {
|
|
46
|
+
const userLocale =
|
|
47
|
+
locale || (typeof navigator !== 'undefined' ? navigator.language : 'en-US');
|
|
48
|
+
|
|
49
|
+
// Vietnamese locale uses Monday as first day
|
|
50
|
+
if (userLocale.startsWith('vi')) {
|
|
51
|
+
return 'monday';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// US, Canada, and some other countries use Sunday
|
|
55
|
+
if (userLocale.startsWith('en-US') || userLocale.startsWith('en-CA')) {
|
|
56
|
+
return 'sunday';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Middle Eastern countries typically use Saturday
|
|
60
|
+
if (userLocale.startsWith('ar') || userLocale.startsWith('he')) {
|
|
61
|
+
return 'saturday';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Most European and other countries use Monday
|
|
65
|
+
return 'monday';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Detects the preferred time format based on locale
|
|
70
|
+
* Uses Intl.DateTimeFormat to determine if the locale uses 12h or 24h format
|
|
71
|
+
*/
|
|
72
|
+
export function detectLocaleTimeFormat(locale?: string): '12h' | '24h' {
|
|
73
|
+
const userLocale =
|
|
74
|
+
locale || (typeof navigator !== 'undefined' ? navigator.language : 'en-US');
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
// Use Intl.DateTimeFormat to detect the locale's preferred format
|
|
78
|
+
const formatter = new Intl.DateTimeFormat(userLocale, { hour: 'numeric' });
|
|
79
|
+
const parts = formatter.formatToParts(new Date(2000, 0, 1, 13, 0));
|
|
80
|
+
const hourPart = parts.find((p) => p.type === 'hour');
|
|
81
|
+
|
|
82
|
+
// If hour is "1" instead of "13", it's 12-hour format
|
|
83
|
+
return hourPart?.value === '1' ? '12h' : '24h';
|
|
84
|
+
} catch {
|
|
85
|
+
// Fallback to 12h for English locales, 24h for others
|
|
86
|
+
return userLocale.startsWith('en') ? '12h' : '24h';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Resolves the effective time format based on user setting
|
|
92
|
+
*/
|
|
93
|
+
export function resolveTimeFormat(
|
|
94
|
+
user?: Pick<CalendarUserSettings, 'time_format'> | null,
|
|
95
|
+
locale?: string
|
|
96
|
+
): '12h' | '24h' {
|
|
97
|
+
// User setting takes priority
|
|
98
|
+
if (user?.time_format && user.time_format !== 'auto') {
|
|
99
|
+
return user.time_format as '12h' | '24h';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Auto-detection based on locale
|
|
103
|
+
return detectLocaleTimeFormat(locale);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolves the effective timezone based on priority system
|
|
108
|
+
*/
|
|
109
|
+
export function resolveTimezone(
|
|
110
|
+
user?: Pick<CalendarUserSettings, 'timezone'> | null,
|
|
111
|
+
workspace?: Pick<CalendarWorkspaceSettings, 'timezone'> | null
|
|
112
|
+
): string {
|
|
113
|
+
// Priority 1: User setting
|
|
114
|
+
if (user?.timezone && user.timezone !== 'auto') {
|
|
115
|
+
return user.timezone;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Priority 2: Workspace setting
|
|
119
|
+
if (workspace?.timezone && workspace.timezone !== 'auto') {
|
|
120
|
+
return workspace.timezone;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Priority 3: Auto-detection
|
|
124
|
+
return detectSystemTimezone();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Resolves the effective first day of week based on priority system
|
|
129
|
+
*/
|
|
130
|
+
export function resolveFirstDayOfWeek(
|
|
131
|
+
user?: Pick<CalendarUserSettings, 'first_day_of_week'> | null,
|
|
132
|
+
workspace?: Pick<CalendarWorkspaceSettings, 'first_day_of_week'> | null,
|
|
133
|
+
locale?: string
|
|
134
|
+
): FirstDayOfWeek {
|
|
135
|
+
// Priority 1: User setting
|
|
136
|
+
if (user?.first_day_of_week && user.first_day_of_week !== 'auto') {
|
|
137
|
+
return user.first_day_of_week as FirstDayOfWeek;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Priority 2: Workspace setting
|
|
141
|
+
if (workspace?.first_day_of_week && workspace.first_day_of_week !== 'auto') {
|
|
142
|
+
return workspace.first_day_of_week as FirstDayOfWeek;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Priority 3: Auto-detection based on locale
|
|
146
|
+
return detectLocaleFirstDay(locale);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolves all calendar settings at once
|
|
151
|
+
*/
|
|
152
|
+
export function resolveCalendarSettings(
|
|
153
|
+
user?: CalendarUserSettings | null,
|
|
154
|
+
workspace?: CalendarWorkspaceSettings | null,
|
|
155
|
+
locale?: string
|
|
156
|
+
): CalendarSettings {
|
|
157
|
+
return {
|
|
158
|
+
timezone: resolveTimezone(user, workspace),
|
|
159
|
+
firstDayOfWeek: resolveFirstDayOfWeek(user, workspace, locale),
|
|
160
|
+
timeFormat: resolveTimeFormat(user, locale),
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Converts first day of week string to number (for Date compatibility)
|
|
166
|
+
* 0 = Sunday, 1 = Monday, 6 = Saturday
|
|
167
|
+
*/
|
|
168
|
+
export function firstDayToNumber(day: FirstDayOfWeek, locale?: string): number {
|
|
169
|
+
if (day === 'auto') {
|
|
170
|
+
day = detectLocaleFirstDay(locale);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
switch (day) {
|
|
174
|
+
case 'sunday':
|
|
175
|
+
return 0;
|
|
176
|
+
case 'monday':
|
|
177
|
+
return 1;
|
|
178
|
+
case 'saturday':
|
|
179
|
+
return 6;
|
|
180
|
+
default:
|
|
181
|
+
return 1; // Default to Monday
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Converts number to first day of week string
|
|
187
|
+
* 0 = Sunday, 1 = Monday, 6 = Saturday
|
|
188
|
+
*/
|
|
189
|
+
export function numberToFirstDay(num: number): FirstDayOfWeek {
|
|
190
|
+
switch (num) {
|
|
191
|
+
case 0:
|
|
192
|
+
return 'sunday';
|
|
193
|
+
case 1:
|
|
194
|
+
return 'monday';
|
|
195
|
+
case 6:
|
|
196
|
+
return 'saturday';
|
|
197
|
+
default:
|
|
198
|
+
return 'monday'; // Default to Monday
|
|
199
|
+
}
|
|
200
|
+
}
|
package/src/internal-domains.ts
CHANGED
|
@@ -16,6 +16,10 @@ export const PRODUCTION_INTERNAL_APP_DOMAINS = [
|
|
|
16
16
|
name: 'cms',
|
|
17
17
|
url: 'https://cms.tuturuuu.com',
|
|
18
18
|
},
|
|
19
|
+
{
|
|
20
|
+
name: 'docs',
|
|
21
|
+
url: 'https://docs.tuturuuu.com',
|
|
22
|
+
},
|
|
19
23
|
{
|
|
20
24
|
name: 'calendar',
|
|
21
25
|
url: 'https://calendar.tuturuuu.com',
|
|
@@ -40,6 +44,10 @@ export const PRODUCTION_INTERNAL_APP_DOMAINS = [
|
|
|
40
44
|
name: 'qr',
|
|
41
45
|
url: 'https://qr.tuturuuu.com',
|
|
42
46
|
},
|
|
47
|
+
{
|
|
48
|
+
name: 'tools',
|
|
49
|
+
url: 'https://tools.tuturuuu.com',
|
|
50
|
+
},
|
|
43
51
|
{
|
|
44
52
|
name: 'nova',
|
|
45
53
|
url: 'https://nova.ai.vn',
|
|
@@ -92,6 +100,14 @@ export const PRODUCTION_INTERNAL_APP_DOMAINS = [
|
|
|
92
100
|
name: 'teach',
|
|
93
101
|
url: 'https://teach.tuturuuu.com',
|
|
94
102
|
},
|
|
103
|
+
{
|
|
104
|
+
name: 'pay',
|
|
105
|
+
url: 'https://pay.tuturuuu.com',
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: 'contacts',
|
|
109
|
+
url: 'https://contacts.tuturuuu.com',
|
|
110
|
+
},
|
|
95
111
|
{
|
|
96
112
|
name: 'hive',
|
|
97
113
|
url: 'https://hive.tuturuuu.com',
|
|
@@ -115,6 +131,10 @@ export const PORTLESS_INTERNAL_APP_DOMAINS = [
|
|
|
115
131
|
name: 'cms',
|
|
116
132
|
url: getTuturuuuPortlessAppOrigin('cms'),
|
|
117
133
|
},
|
|
134
|
+
{
|
|
135
|
+
name: 'docs',
|
|
136
|
+
url: getTuturuuuPortlessAppOrigin('docs'),
|
|
137
|
+
},
|
|
118
138
|
{
|
|
119
139
|
name: 'calendar',
|
|
120
140
|
url: getTuturuuuPortlessAppOrigin('calendar'),
|
|
@@ -139,6 +159,10 @@ export const PORTLESS_INTERNAL_APP_DOMAINS = [
|
|
|
139
159
|
name: 'qr',
|
|
140
160
|
url: getTuturuuuPortlessAppOrigin('qr'),
|
|
141
161
|
},
|
|
162
|
+
{
|
|
163
|
+
name: 'tools',
|
|
164
|
+
url: getTuturuuuPortlessAppOrigin('tools'),
|
|
165
|
+
},
|
|
142
166
|
{
|
|
143
167
|
name: 'nova',
|
|
144
168
|
url: getTuturuuuPortlessAppOrigin('nova'),
|
|
@@ -183,6 +207,14 @@ export const PORTLESS_INTERNAL_APP_DOMAINS = [
|
|
|
183
207
|
name: 'teach',
|
|
184
208
|
url: getTuturuuuPortlessAppOrigin('teach'),
|
|
185
209
|
},
|
|
210
|
+
{
|
|
211
|
+
name: 'pay',
|
|
212
|
+
url: getTuturuuuPortlessAppOrigin('pay'),
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
name: 'contacts',
|
|
216
|
+
url: getTuturuuuPortlessAppOrigin('contacts'),
|
|
217
|
+
},
|
|
186
218
|
{
|
|
187
219
|
name: 'hive',
|
|
188
220
|
url: getTuturuuuPortlessAppOrigin('hive'),
|
|
@@ -226,6 +258,10 @@ export const LOCALHOST_INTERNAL_APP_DOMAINS = [
|
|
|
226
258
|
name: 'qr',
|
|
227
259
|
url: 'http://localhost:7819',
|
|
228
260
|
},
|
|
261
|
+
{
|
|
262
|
+
name: 'tools',
|
|
263
|
+
url: 'http://localhost:7825',
|
|
264
|
+
},
|
|
229
265
|
{
|
|
230
266
|
name: 'platform',
|
|
231
267
|
url: 'http://localhost:7803',
|
|
@@ -274,6 +310,14 @@ export const LOCALHOST_INTERNAL_APP_DOMAINS = [
|
|
|
274
310
|
name: 'teach',
|
|
275
311
|
url: 'http://localhost:7813',
|
|
276
312
|
},
|
|
313
|
+
{
|
|
314
|
+
name: 'pay',
|
|
315
|
+
url: 'http://localhost:7826',
|
|
316
|
+
},
|
|
317
|
+
{
|
|
318
|
+
name: 'contacts',
|
|
319
|
+
url: 'http://localhost:7827',
|
|
320
|
+
},
|
|
277
321
|
{
|
|
278
322
|
name: 'hive',
|
|
279
323
|
url: 'http://localhost:7814',
|
|
@@ -3,6 +3,8 @@ import {
|
|
|
3
3
|
getLaunchableApp,
|
|
4
4
|
getLaunchableAppByTitle,
|
|
5
5
|
getLaunchableAppOrigin,
|
|
6
|
+
LAUNCHABLE_APP_CATEGORIES,
|
|
7
|
+
LAUNCHABLE_APPS,
|
|
6
8
|
type LaunchableWorkspace,
|
|
7
9
|
resolveLaunchableAppPath,
|
|
8
10
|
resolveLaunchableAppUrl,
|
|
@@ -27,6 +29,39 @@ describe('launchable apps', () => {
|
|
|
27
29
|
expect(getLaunchableAppByTitle('apps gateway')?.slug).toBe('apps');
|
|
28
30
|
});
|
|
29
31
|
|
|
32
|
+
it('keeps retired apps out of the catalog and pins category overrides', () => {
|
|
33
|
+
expect(LAUNCHABLE_APPS.map((app) => app.slug)).not.toContain('external');
|
|
34
|
+
expect(LAUNCHABLE_APPS.map((app) => app.slug)).not.toContain('playground');
|
|
35
|
+
expect(LAUNCHABLE_APP_CATEGORIES).not.toContain('core');
|
|
36
|
+
expect(LAUNCHABLE_APP_CATEGORIES).not.toContain('content');
|
|
37
|
+
expect(LAUNCHABLE_APP_CATEGORIES).not.toContain('developer');
|
|
38
|
+
expect(LAUNCHABLE_APP_CATEGORIES).toContain('miscellaneous');
|
|
39
|
+
expect(getLaunchableApp('external')).toBeNull();
|
|
40
|
+
expect(getLaunchableApp('playground')).toBeNull();
|
|
41
|
+
expect(getLaunchableApp('platform')?.category).toBe('productivity');
|
|
42
|
+
expect(getLaunchableApp('apps')?.category).toBe('miscellaneous');
|
|
43
|
+
expect(getLaunchableApp('cms')?.category).toBe('operations');
|
|
44
|
+
expect(getLaunchableApp('rewise')?.category).toBe('ai');
|
|
45
|
+
expect(getLaunchableApp('docs')?.category).toBe('miscellaneous');
|
|
46
|
+
expect(getLaunchableApp('tools')?.category).toBe('miscellaneous');
|
|
47
|
+
expect(getLaunchableApp('shortener')?.category).toBe('miscellaneous');
|
|
48
|
+
expect(
|
|
49
|
+
LAUNCHABLE_APPS.filter((app) => app.category === 'productivity').map(
|
|
50
|
+
(app) => app.slug
|
|
51
|
+
)[0]
|
|
52
|
+
).toBe('platform');
|
|
53
|
+
expect(
|
|
54
|
+
LAUNCHABLE_APPS.filter((app) => app.category === 'operations').map(
|
|
55
|
+
(app) => app.slug
|
|
56
|
+
)
|
|
57
|
+
).not.toContain('apps');
|
|
58
|
+
expect(
|
|
59
|
+
LAUNCHABLE_APPS.filter((app) => app.category === 'miscellaneous').map(
|
|
60
|
+
(app) => app.slug
|
|
61
|
+
)[0]
|
|
62
|
+
).toBe('apps');
|
|
63
|
+
});
|
|
64
|
+
|
|
30
65
|
it('resolves production, Portless, and local origins', () => {
|
|
31
66
|
const app = getLaunchableApp('calendar');
|
|
32
67
|
|
|
@@ -46,6 +81,15 @@ describe('launchable apps', () => {
|
|
|
46
81
|
expect(getLaunchableAppOrigin(meet!, { environment: 'production' })).toBe(
|
|
47
82
|
'https://meet.tuturuuu.com'
|
|
48
83
|
);
|
|
84
|
+
|
|
85
|
+
const docs = getLaunchableApp('docs');
|
|
86
|
+
expect(docs).not.toBeNull();
|
|
87
|
+
expect(getLaunchableAppOrigin(docs!, { environment: 'production' })).toBe(
|
|
88
|
+
'https://docs.tuturuuu.com'
|
|
89
|
+
);
|
|
90
|
+
expect(getLaunchableAppOrigin(docs!, { environment: 'portless' })).toBe(
|
|
91
|
+
'https://docs.tuturuuu.localhost'
|
|
92
|
+
);
|
|
49
93
|
});
|
|
50
94
|
|
|
51
95
|
it('detects Portless dev from the current origin', () => {
|
package/src/launchable-apps.ts
CHANGED
|
@@ -2,13 +2,11 @@ import { toWorkspaceSlug } from './constants';
|
|
|
2
2
|
import { getTuturuuuPortlessAppOrigin } from './portless';
|
|
3
3
|
|
|
4
4
|
export const LAUNCHABLE_APP_CATEGORIES = [
|
|
5
|
-
'core',
|
|
6
5
|
'productivity',
|
|
7
|
-
'content',
|
|
8
6
|
'operations',
|
|
9
7
|
'learning',
|
|
10
|
-
'developer',
|
|
11
8
|
'ai',
|
|
9
|
+
'miscellaneous',
|
|
12
10
|
] as const;
|
|
13
11
|
|
|
14
12
|
export type LaunchableAppCategory = (typeof LAUNCHABLE_APP_CATEGORIES)[number];
|
|
@@ -53,7 +51,7 @@ export const LAUNCHABLE_APPS = [
|
|
|
53
51
|
{
|
|
54
52
|
aliases: ['Tuturuuu', 'Dashboard', 'Workspace'],
|
|
55
53
|
appRoot: 'apps/web',
|
|
56
|
-
category: '
|
|
54
|
+
category: 'productivity',
|
|
57
55
|
defaultPath: '/',
|
|
58
56
|
localhostOrigin: 'http://localhost:7803',
|
|
59
57
|
packageName: '@tuturuuu/web',
|
|
@@ -63,18 +61,6 @@ export const LAUNCHABLE_APPS = [
|
|
|
63
61
|
title: 'Platform',
|
|
64
62
|
workspacePathResolver: workspaceRootPath,
|
|
65
63
|
},
|
|
66
|
-
{
|
|
67
|
-
aliases: ['App Launcher', 'Gateway', 'Apps Gateway'],
|
|
68
|
-
appRoot: 'apps/apps',
|
|
69
|
-
category: 'core',
|
|
70
|
-
defaultPath: '/',
|
|
71
|
-
localhostOrigin: 'http://localhost:7818',
|
|
72
|
-
packageName: '@tuturuuu/apps',
|
|
73
|
-
portlessApp: 'apps',
|
|
74
|
-
productionUrl: 'https://apps.tuturuuu.com',
|
|
75
|
-
slug: 'apps',
|
|
76
|
-
title: 'Apps',
|
|
77
|
-
},
|
|
78
64
|
{
|
|
79
65
|
aliases: ['Schedule', 'Events'],
|
|
80
66
|
appRoot: 'apps/calendar',
|
|
@@ -104,7 +90,7 @@ export const LAUNCHABLE_APPS = [
|
|
|
104
90
|
{
|
|
105
91
|
aliases: ['Content', 'CMS'],
|
|
106
92
|
appRoot: 'apps/cms',
|
|
107
|
-
category: '
|
|
93
|
+
category: 'operations',
|
|
108
94
|
defaultPath: '/personal',
|
|
109
95
|
localhostOrigin: 'http://localhost:7811',
|
|
110
96
|
packageName: '@tuturuuu/cms',
|
|
@@ -128,15 +114,27 @@ export const LAUNCHABLE_APPS = [
|
|
|
128
114
|
workspacePathResolver: workspaceRootPath,
|
|
129
115
|
},
|
|
130
116
|
{
|
|
131
|
-
aliases: ['
|
|
132
|
-
appRoot: 'apps/
|
|
133
|
-
category: '
|
|
117
|
+
aliases: ['App Launcher', 'Gateway', 'Apps Gateway'],
|
|
118
|
+
appRoot: 'apps/apps',
|
|
119
|
+
category: 'miscellaneous',
|
|
134
120
|
defaultPath: '/',
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
121
|
+
localhostOrigin: 'http://localhost:7818',
|
|
122
|
+
packageName: '@tuturuuu/apps',
|
|
123
|
+
portlessApp: 'apps',
|
|
124
|
+
productionUrl: 'https://apps.tuturuuu.com',
|
|
125
|
+
slug: 'apps',
|
|
126
|
+
title: 'Apps',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
aliases: ['Documentation', 'Guides', 'Reference'],
|
|
130
|
+
appRoot: 'apps/docs',
|
|
131
|
+
category: 'miscellaneous',
|
|
132
|
+
defaultPath: '/',
|
|
133
|
+
packageName: 'apps/docs',
|
|
134
|
+
portlessApp: 'docs',
|
|
135
|
+
productionUrl: 'https://docs.tuturuuu.com',
|
|
136
|
+
slug: 'docs',
|
|
137
|
+
title: 'Docs',
|
|
140
138
|
},
|
|
141
139
|
{
|
|
142
140
|
aliases: ['Money', 'Wallets', 'Invoices'],
|
|
@@ -252,32 +250,21 @@ export const LAUNCHABLE_APPS = [
|
|
|
252
250
|
title: 'Nova',
|
|
253
251
|
},
|
|
254
252
|
{
|
|
255
|
-
aliases: ['
|
|
256
|
-
appRoot: 'apps/
|
|
257
|
-
category: '
|
|
258
|
-
defaultPath: '/',
|
|
259
|
-
packageName: '@tuturuuu/playground',
|
|
260
|
-
portlessApp: 'playground',
|
|
261
|
-
productionUrl: 'https://playground.tuturuuu.com',
|
|
262
|
-
slug: 'playground',
|
|
263
|
-
title: 'Playground',
|
|
264
|
-
},
|
|
265
|
-
{
|
|
266
|
-
aliases: ['QR Code', 'Generator'],
|
|
267
|
-
appRoot: 'apps/qr',
|
|
268
|
-
category: 'developer',
|
|
253
|
+
aliases: ['QR Code', 'Generator', 'Password', 'Random', 'UUID', 'Token'],
|
|
254
|
+
appRoot: 'apps/tools',
|
|
255
|
+
category: 'miscellaneous',
|
|
269
256
|
defaultPath: '/',
|
|
270
|
-
localhostOrigin: 'http://localhost:
|
|
271
|
-
packageName: '@tuturuuu/
|
|
272
|
-
portlessApp: '
|
|
273
|
-
productionUrl: 'https://
|
|
274
|
-
slug: '
|
|
275
|
-
title: '
|
|
257
|
+
localhostOrigin: 'http://localhost:7825',
|
|
258
|
+
packageName: '@tuturuuu/tools',
|
|
259
|
+
portlessApp: 'tools',
|
|
260
|
+
productionUrl: 'https://tools.tuturuuu.com',
|
|
261
|
+
slug: 'tools',
|
|
262
|
+
title: 'Tools',
|
|
276
263
|
},
|
|
277
264
|
{
|
|
278
265
|
aliases: ['Review', 'Flashcards'],
|
|
279
266
|
appRoot: 'apps/rewise',
|
|
280
|
-
category: '
|
|
267
|
+
category: 'ai',
|
|
281
268
|
defaultPath: '/',
|
|
282
269
|
localhostOrigin: 'http://localhost:7804',
|
|
283
270
|
packageName: '@tuturuuu/rewise',
|
|
@@ -289,7 +276,7 @@ export const LAUNCHABLE_APPS = [
|
|
|
289
276
|
{
|
|
290
277
|
aliases: ['Links', 'URL Shortener'],
|
|
291
278
|
appRoot: 'apps/shortener',
|
|
292
|
-
category: '
|
|
279
|
+
category: 'miscellaneous',
|
|
293
280
|
defaultPath: '/',
|
|
294
281
|
packageName: '@tuturuuu/shortener',
|
|
295
282
|
portlessApp: 'shortener',
|
|
@@ -323,6 +310,32 @@ export const LAUNCHABLE_APPS = [
|
|
|
323
310
|
title: 'Teach',
|
|
324
311
|
workspacePathResolver: workspaceRootPath,
|
|
325
312
|
},
|
|
313
|
+
{
|
|
314
|
+
aliases: ['Billing', 'Subscriptions', 'Payments'],
|
|
315
|
+
appRoot: 'apps/pay',
|
|
316
|
+
category: 'operations',
|
|
317
|
+
defaultPath: '/personal',
|
|
318
|
+
localhostOrigin: 'http://localhost:7826',
|
|
319
|
+
packageName: '@tuturuuu/pay',
|
|
320
|
+
portlessApp: 'pay',
|
|
321
|
+
productionUrl: 'https://pay.tuturuuu.com',
|
|
322
|
+
slug: 'pay',
|
|
323
|
+
title: 'Pay',
|
|
324
|
+
workspacePathResolver: workspaceRootPath,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
aliases: ['CRM', 'Users', 'Members', 'Customers', 'Leads'],
|
|
328
|
+
appRoot: 'apps/contacts',
|
|
329
|
+
category: 'operations',
|
|
330
|
+
defaultPath: '/personal',
|
|
331
|
+
localhostOrigin: 'http://localhost:7827',
|
|
332
|
+
packageName: '@tuturuuu/contacts',
|
|
333
|
+
portlessApp: 'contacts',
|
|
334
|
+
productionUrl: 'https://contacts.tuturuuu.com',
|
|
335
|
+
slug: 'contacts',
|
|
336
|
+
title: 'Contacts',
|
|
337
|
+
workspacePathResolver: workspaceRootPath,
|
|
338
|
+
},
|
|
326
339
|
{
|
|
327
340
|
aliases: ['Time Tracking', 'Timer'],
|
|
328
341
|
appRoot: 'apps/track',
|
|
@@ -7,10 +7,10 @@ import {
|
|
|
7
7
|
|
|
8
8
|
describe('platform release metadata', () => {
|
|
9
9
|
it('uses the centralized shared browser app version', () => {
|
|
10
|
-
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.
|
|
10
|
+
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.21.0'); // x-release-please-version
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
it('
|
|
13
|
+
it('preserves the source timestamp in builtAt and derives a short hash', () => {
|
|
14
14
|
expect(
|
|
15
15
|
normalizePlatformBuildMetadata({
|
|
16
16
|
builtAt: '2026-05-27T10:00:00.000Z',
|
package/src/platform-release.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
|
|
2
2
|
|
|
3
|
-
export const TUTURUUU_PLATFORM_VERSION = '0.
|
|
3
|
+
export const TUTURUUU_PLATFORM_VERSION = '0.21.0'; // x-release-please-version
|
|
4
4
|
|
|
5
5
|
export type PlatformBuildMetadataInput = {
|
|
6
|
+
/** ISO timestamp of the source commit used for the build, not deployment time. */
|
|
6
7
|
builtAt?: string | null;
|
|
7
8
|
commitHash?: string | null;
|
|
8
9
|
commitMessage?: string | null;
|
|
@@ -14,6 +15,7 @@ export type PlatformBuildMetadataInput = {
|
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export type PlatformBuildMetadata = {
|
|
18
|
+
/** ISO timestamp of the source commit used for the build, not deployment time. */
|
|
17
19
|
builtAt: string;
|
|
18
20
|
commitHash: string;
|
|
19
21
|
commitMessage: string;
|
package/src/portless.ts
CHANGED
|
@@ -10,6 +10,8 @@ export const TUTURUUU_PORTLESS_APP_ORIGINS = {
|
|
|
10
10
|
calendar: `https://calendar.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
11
11
|
chat: `https://chat.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
12
12
|
cms: `https://cms.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
13
|
+
contacts: `https://contacts.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
14
|
+
docs: `https://docs.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
13
15
|
drive: `https://drive.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
14
16
|
external: `https://external.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
15
17
|
finance: `https://finance.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
@@ -22,10 +24,12 @@ export const TUTURUUU_PORTLESS_APP_ORIGINS = {
|
|
|
22
24
|
meet: `https://meet.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
23
25
|
mind: `https://mind.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
24
26
|
nova: `https://nova.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
27
|
+
pay: `https://pay.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
25
28
|
platform: TUTURUUU_PORTLESS_ROOT_ORIGIN,
|
|
26
29
|
playground: `https://playground.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
27
30
|
qr: `https://qr.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
28
31
|
rewise: `https://rewise.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
32
|
+
tools: `https://tools.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
29
33
|
shortener: `https://shortener.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
30
34
|
storefront: `https://storefront.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
31
35
|
'tanstack-web': `https://tanstack.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|