@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.
@@ -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
+ }
@@ -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', () => {
@@ -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: 'core',
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: 'content',
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: ['External Projects', 'WebGL'],
132
- appRoot: 'apps/external',
133
- category: 'developer',
117
+ aliases: ['App Launcher', 'Gateway', 'Apps Gateway'],
118
+ appRoot: 'apps/apps',
119
+ category: 'miscellaneous',
134
120
  defaultPath: '/',
135
- packageName: '@tuturuuu/external',
136
- portlessApp: 'external',
137
- productionUrl: 'https://external.tuturuuu.com',
138
- slug: 'external',
139
- title: 'External',
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: ['Labs', 'Sandbox'],
256
- appRoot: 'apps/playground',
257
- category: 'developer',
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:7819',
271
- packageName: '@tuturuuu/qr',
272
- portlessApp: 'qr',
273
- productionUrl: 'https://qr.tuturuuu.com',
274
- slug: 'qr',
275
- title: 'QR',
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: 'learning',
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: 'developer',
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.17.0'); // x-release-please-version
10
+ expect(TUTURUUU_PLATFORM_VERSION).toBe('0.21.0'); // x-release-please-version
11
11
  });
12
12
 
13
- it('normalizes generated metadata and derives a short hash', () => {
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',
@@ -1,8 +1,9 @@
1
1
  import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
2
2
 
3
- export const TUTURUUU_PLATFORM_VERSION = '0.17.0'; // x-release-please-version
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}`,