@tuturuuu/utils 0.21.0 → 0.22.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 CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.22.0](https://github.com/tutur3u/platform/compare/utils-v0.21.0...utils-v0.22.0) (2026-08-04)
4
+
5
+
6
+ ### Features
7
+
8
+ * **chat:** add connected site sync bridge ([#5078](https://github.com/tutur3u/platform/issues/5078)) ([3de1a07](https://github.com/tutur3u/platform/commit/3de1a07dfa2fec96982aa0d116e003d0aa3fe47c))
9
+ * **onboarding:** connect product guidance across apps ([68cf626](https://github.com/tutur3u/platform/commit/68cf626c9650e5044b6c123f9423a6cebf1bba9a))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **ai:** accept rich embedding payloads ([a65b13f](https://github.com/tutur3u/platform/commit/a65b13fa80034959e5f18ee4515b3081002558e2))
15
+ * **tooling:** support webpack verification builds ([5275e10](https://github.com/tutur3u/platform/commit/5275e10c4007ae92e84be6c057a79f223980700c))
16
+
3
17
  ## [0.21.0](https://github.com/tutur3u/platform/compare/utils-v0.20.0...utils-v0.21.0) (2026-07-28)
4
18
 
5
19
 
package/biome.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "root": false,
3
- "$schema": "https://biomejs.dev/schemas/2.5.5/schema.json",
3
+ "$schema": "https://biomejs.dev/schemas/2.5.6/schema.json",
4
4
  "extends": "//"
5
5
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tuturuuu/utils",
3
3
  "license": "MIT",
4
- "version": "0.21.0",
4
+ "version": "0.22.0",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "https://github.com/tutur3u/platform",
@@ -20,21 +20,21 @@
20
20
  "@noble/ciphers": "^2.2.0",
21
21
  "@noble/hashes": "^2.2.0",
22
22
  "@tanstack/react-query": "^5.101.4",
23
- "@tiptap/core": "3.28.0",
24
- "@tiptap/react": "3.28.0",
23
+ "@tiptap/core": "3.29.2",
24
+ "@tiptap/react": "3.29.2",
25
25
  "@tuturuuu/google": "0.1.0",
26
26
  "@tuturuuu/icons": "0.1.0",
27
- "@tuturuuu/internal-api": "0.25.0",
27
+ "@tuturuuu/internal-api": "0.27.0",
28
28
  "@tuturuuu/supabase": "0.5.0",
29
29
  "@upstash/ratelimit": "^2.0.8",
30
- "@upstash/redis": "^1.38.0",
30
+ "@upstash/redis": "^1.38.1",
31
31
  "clsx": "^2.1.1",
32
32
  "date-fns": "^4.4.0",
33
33
  "dayjs": "^1.11.21",
34
34
  "diff": "^9.0.0",
35
- "effect": "^3.22.0",
35
+ "effect": "^3.22.1",
36
36
  "moment": "^2.30.1",
37
- "next": "^16.3.0-preview.9",
37
+ "next": "^16.3.0",
38
38
  "prosemirror-model": "^1.25.11",
39
39
  "react": "^19.2.8",
40
40
  "react-dom": "^19.2.8",
@@ -45,14 +45,14 @@
45
45
  "zod": "^4.4.3"
46
46
  },
47
47
  "devDependencies": {
48
- "@tuturuuu/types": "0.24.0",
48
+ "@tuturuuu/types": "0.26.0",
49
49
  "@tuturuuu/typescript-config": "0.1.1",
50
50
  "@types/diff": "^8.0.0",
51
- "@types/node": "^26.1.1",
52
- "@types/react": "^19.2.17",
53
- "@vitejs/plugin-react": "^6.0.4",
51
+ "@types/node": "^26.1.2",
52
+ "@types/react": "^19.2.18",
53
+ "@vitejs/plugin-react": "^6.0.5",
54
54
  "typescript": "7.0.2",
55
- "vite": "^8.1.5",
55
+ "vite": "^8.2.0",
56
56
  "vitest": "^4.1.10"
57
57
  },
58
58
  "exports": {
@@ -159,6 +159,35 @@ describe('guardApiProxyRequest', () => {
159
159
  expect(mocks.validateEmoji).not.toHaveBeenCalled();
160
160
  });
161
161
 
162
+ it('can skip form-content validation without skipping payload limits', async () => {
163
+ mocks.validateEmoji.mockResolvedValue(
164
+ new Response(JSON.stringify({ error: 'Bad Request' }), { status: 400 })
165
+ );
166
+
167
+ const { guardApiProxyRequest } = await import('../api-proxy-guard.js');
168
+ const response = await guardApiProxyRequest(
169
+ makeRequest('/v1/embeddings', 'POST'),
170
+ {
171
+ prefixBase: 'proxy:test:ai',
172
+ skipContentValidation: true,
173
+ }
174
+ );
175
+
176
+ expect(response).toBeNull();
177
+ expect(mocks.validateEmoji).not.toHaveBeenCalled();
178
+
179
+ const oversized = await guardApiProxyRequest(
180
+ makeRequest('/v1/embeddings', 'POST', {
181
+ 'content-length': `${1024 * 1024 + 1}`,
182
+ }),
183
+ {
184
+ prefixBase: 'proxy:test:ai',
185
+ skipContentValidation: true,
186
+ }
187
+ );
188
+ expect(oversized?.status).toBe(413);
189
+ });
190
+
162
191
  it('returns an IP-block response when the client is blocked', async () => {
163
192
  vi.stubEnv('NODE_ENV', 'production');
164
193
  mocks.extractIp.mockReturnValue('1.2.3.4');
@@ -88,6 +88,7 @@ export type GuardOptions = {
88
88
  additionalRoutePolicies?: ProxyRoutePolicy[];
89
89
  prefixBase: string;
90
90
  routePolicies?: ProxyRoutePolicy[];
91
+ skipContentValidation?: boolean;
91
92
  trustedBypassRules?: TrustedProxyBypassRule[];
92
93
  };
93
94
 
@@ -1385,6 +1386,10 @@ export async function guardApiProxyRequest(
1385
1386
  ? ['snapshot']
1386
1387
  : undefined;
1387
1388
 
1389
+ if (options.skipContentValidation) {
1390
+ return null;
1391
+ }
1392
+
1388
1393
  return (
1389
1394
  (await validateRequestEmojiLimit(req, {
1390
1395
  allowDescriptionYjsState,
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import {
3
3
  getLaunchableApp,
4
+ getLaunchableAppByHostname,
4
5
  getLaunchableAppByTitle,
5
6
  getLaunchableAppOrigin,
6
7
  LAUNCHABLE_APP_CATEGORIES,
@@ -36,6 +37,16 @@ describe('launchable apps', () => {
36
37
  expect(getLaunchableAppByTitle('apps gateway')?.slug).toBe('apps');
37
38
  });
38
39
 
40
+ it('resolves production and Portless hostnames', () => {
41
+ expect(getLaunchableAppByHostname('calendar.tuturuuu.com')?.slug).toBe(
42
+ 'calendar'
43
+ );
44
+ expect(getLaunchableAppByHostname('finance.tuturuuu.localhost')?.slug).toBe(
45
+ 'finance'
46
+ );
47
+ expect(getLaunchableAppByHostname('localhost:7808')?.slug).toBe('finance');
48
+ });
49
+
39
50
  it('keeps retired apps out of the catalog and pins category overrides', () => {
40
51
  expect(LAUNCHABLE_APPS.map((app) => app.slug)).not.toContain('external');
41
52
  expect(LAUNCHABLE_APPS.map((app) => app.slug)).not.toContain('playground');
@@ -416,6 +416,29 @@ export function getLaunchableAppByTitle(value?: string | null) {
416
416
  );
417
417
  }
418
418
 
419
+ export function getLaunchableAppByHostname(hostname: string) {
420
+ const normalized = hostname.trim().toLowerCase();
421
+ if (!normalized) return null;
422
+ const normalizedHostname = normalized.split(':')[0];
423
+
424
+ return (
425
+ LAUNCHABLE_APPS.find((app) => {
426
+ if (normalizedHostname === new URL(app.productionUrl).hostname)
427
+ return true;
428
+ if (
429
+ normalizedHostname === `${app.portlessApp}.tuturuuu.localhost` ||
430
+ normalizedHostname === `${app.slug}.tuturuuu.localhost`
431
+ ) {
432
+ return true;
433
+ }
434
+
435
+ return 'localhostOrigin' in app && app.localhostOrigin
436
+ ? normalized === new URL(app.localhostOrigin).host
437
+ : false;
438
+ }) ?? null
439
+ );
440
+ }
441
+
419
442
  function getAutoEnvironment(currentOrigin?: string): LaunchableAppEnvironment {
420
443
  if (currentOrigin?.includes('tuturuuu.localhost')) return 'portless';
421
444
  if (currentOrigin?.includes('localhost')) return 'localhost';
@@ -4,6 +4,7 @@ import {
4
4
  createTuturuuuNextConfig,
5
5
  isTuturuuuNextCacheComponentsEnabled,
6
6
  isTuturuuuNextReactCompilerEnabled,
7
+ isTuturuuuTurbopackRustReactCompilerEnabled,
7
8
  resolveTuturuuuInfrastructureAppUrl,
8
9
  resolveTuturuuuWebAppUrl,
9
10
  TUTURUUU_NEXT_IMAGE_REMOTE_PATTERNS,
@@ -267,6 +268,20 @@ describe('isTuturuuuNextCacheComponentsEnabled', () => {
267
268
  });
268
269
  });
269
270
 
271
+ describe('isTuturuuuTurbopackRustReactCompilerEnabled', () => {
272
+ it('keeps the native compiler enabled for normal builds', () => {
273
+ expect(isTuturuuuTurbopackRustReactCompilerEnabled({})).toBe(true);
274
+ });
275
+
276
+ it('disables the Turbopack-only compiler for webpack fallback builds', () => {
277
+ expect(
278
+ isTuturuuuTurbopackRustReactCompilerEnabled({
279
+ NEXT_WEBPACK_BUILD: '1',
280
+ })
281
+ ).toBe(false);
282
+ });
283
+ });
284
+
270
285
  describe('isTuturuuuNextReactCompilerEnabled', () => {
271
286
  it('keeps React Compiler enabled for next dev', () => {
272
287
  expect(
@@ -83,6 +83,12 @@ export function isTuturuuuNextCacheComponentsEnabled(
83
83
  return true;
84
84
  }
85
85
 
86
+ export function isTuturuuuTurbopackRustReactCompilerEnabled(
87
+ env: Environment = process.env
88
+ ) {
89
+ return env.NEXT_WEBPACK_BUILD !== '1';
90
+ }
91
+
86
92
  export function createTuturuuuNextConfig(config: NextConfig = {}): NextConfig {
87
93
  const experimentalConfig = config.experimental ?? {};
88
94
  const imageConfig = config.images ?? {};
@@ -116,7 +122,8 @@ export function createTuturuuuNextConfig(config: NextConfig = {}): NextConfig {
116
122
  turbopackFileSystemCacheForBuild:
117
123
  experimentalConfig.turbopackFileSystemCacheForBuild ?? true,
118
124
  turbopackRustReactCompiler:
119
- experimentalConfig.turbopackRustReactCompiler ?? true,
125
+ experimentalConfig.turbopackRustReactCompiler ??
126
+ isTuturuuuTurbopackRustReactCompilerEnabled(),
120
127
  optimizePackageImports: mergeStringArrays(
121
128
  TUTURUUU_NEXT_OPTIMIZE_PACKAGE_IMPORTS,
122
129
  experimentalConfig.optimizePackageImports
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { LAUNCHABLE_APPS } from './launchable-apps';
3
+ import {
4
+ ONBOARDING_MISSIONS,
5
+ ONBOARDING_PERSONAS,
6
+ recommendOnboardingApps,
7
+ } from './onboarding';
8
+
9
+ describe('connected onboarding manifest', () => {
10
+ it('has one dedicated mission for every launchable app', () => {
11
+ expect(ONBOARDING_MISSIONS.map(({ appSlug }) => appSlug).sort()).toEqual(
12
+ LAUNCHABLE_APPS.map(({ slug }) => slug).sort()
13
+ );
14
+ });
15
+
16
+ it('keeps every mission short and action-oriented', () => {
17
+ expect(
18
+ ONBOARDING_MISSIONS.every(({ capabilities }) => capabilities.length === 3)
19
+ ).toBe(true);
20
+ });
21
+
22
+ it.each(ONBOARDING_PERSONAS)(
23
+ 'returns launchable recommendations for %s',
24
+ (persona) => {
25
+ const recommendations = recommendOnboardingApps({
26
+ goals: [],
27
+ persona,
28
+ });
29
+ const launchable = new Set(LAUNCHABLE_APPS.map(({ slug }) => slug));
30
+
31
+ expect(recommendations.length).toBeGreaterThan(0);
32
+ expect(recommendations.every((slug) => launchable.has(slug))).toBe(true);
33
+ }
34
+ );
35
+ });
@@ -0,0 +1,199 @@
1
+ import { LAUNCHABLE_APPS } from './launchable-apps';
2
+
3
+ export const ONBOARDING_PERSONAS = [
4
+ 'professional',
5
+ 'student',
6
+ 'founder',
7
+ 'executive',
8
+ 'team_leader',
9
+ 'educator',
10
+ 'creator',
11
+ 'developer',
12
+ 'operations',
13
+ ] as const;
14
+
15
+ export const ONBOARDING_GOALS = [
16
+ 'focus',
17
+ 'collaborate',
18
+ 'operate',
19
+ 'learn',
20
+ 'build',
21
+ ] as const;
22
+
23
+ export type OnboardingPersona = (typeof ONBOARDING_PERSONAS)[number];
24
+ export type OnboardingGoal = (typeof ONBOARDING_GOALS)[number];
25
+ export type OnboardingAppSlug = (typeof LAUNCHABLE_APPS)[number]['slug'];
26
+ export type OnboardingActionSafety =
27
+ | 'preview'
28
+ | 'reversible'
29
+ | 'confirmation_required';
30
+
31
+ export type OnboardingMission = {
32
+ appSlug: OnboardingAppSlug;
33
+ capabilities: readonly [string, string, string];
34
+ goal: OnboardingGoal;
35
+ safety: OnboardingActionSafety;
36
+ };
37
+
38
+ export type OnboardingJourneyPreferences = {
39
+ goals: OnboardingGoal[];
40
+ persona: OnboardingPersona | null;
41
+ };
42
+
43
+ export type OnboardingJourneyProgress = OnboardingJourneyPreferences & {
44
+ completedMissionIds: string[];
45
+ dismissedAt: string | null;
46
+ mode: 'standard' | 'employee_test';
47
+ replayApp: OnboardingAppSlug | null;
48
+ revision: number;
49
+ };
50
+
51
+ export const ONBOARDING_JOURNEY_REVISION = 2;
52
+
53
+ export const PERSONA_RECOMMENDATIONS = {
54
+ professional: ['tasks', 'calendar', 'mail', 'drive', 'assistant'],
55
+ student: ['learn', 'rewise', 'calendar', 'tasks', 'mind'],
56
+ founder: ['tasks', 'finance', 'contacts', 'forms', 'assistant'],
57
+ executive: ['platform', 'finance', 'calendar', 'meet', 'assistant'],
58
+ team_leader: ['tasks', 'chat', 'calendar', 'meet', 'contacts'],
59
+ educator: ['teach', 'learn', 'forms', 'meet', 'drive'],
60
+ creator: ['cms', 'drive', 'shortener', 'forms', 'assistant'],
61
+ developer: ['git', 'ai', 'docs', 'tasks', 'tools'],
62
+ operations: ['inventory', 'finance', 'contacts', 'forms', 'tasks'],
63
+ } as const satisfies Record<OnboardingPersona, readonly string[]>;
64
+
65
+ export const GOAL_RECOMMENDATIONS = {
66
+ focus: ['tasks', 'calendar', 'mail', 'track', 'assistant'],
67
+ collaborate: ['chat', 'meet', 'drive', 'tasks', 'forms'],
68
+ operate: ['finance', 'inventory', 'contacts', 'pay', 'storefront'],
69
+ learn: ['learn', 'teach', 'rewise', 'mind', 'docs'],
70
+ build: ['ai', 'git', 'cms', 'nova', 'tools'],
71
+ } as const satisfies Record<OnboardingGoal, readonly string[]>;
72
+
73
+ const MISSIONS = [
74
+ ['platform', 'focus', 'reversible', ['overview', 'apps', 'next_move']],
75
+ ['calendar', 'focus', 'reversible', ['schedule', 'timebox', 'review']],
76
+ [
77
+ 'chat',
78
+ 'collaborate',
79
+ 'confirmation_required',
80
+ ['spaces', 'message', 'follow_up'],
81
+ ],
82
+ ['cms', 'build', 'confirmation_required', ['content', 'preview', 'publish']],
83
+ [
84
+ 'drive',
85
+ 'collaborate',
86
+ 'confirmation_required',
87
+ ['organize', 'share', 'find'],
88
+ ],
89
+ ['git', 'build', 'preview', ['repositories', 'browse', 'search']],
90
+ ['apps', 'focus', 'reversible', ['discover', 'filter', 'launch']],
91
+ ['docs', 'learn', 'preview', ['browse', 'search', 'reference']],
92
+ [
93
+ 'finance',
94
+ 'operate',
95
+ 'confirmation_required',
96
+ ['wallets', 'transactions', 'reports'],
97
+ ],
98
+ ['hive', 'build', 'preview', ['world', 'simulate', 'observe']],
99
+ [
100
+ 'inventory',
101
+ 'operate',
102
+ 'confirmation_required',
103
+ ['catalog', 'stock', 'movement'],
104
+ ],
105
+ [
106
+ 'storefront',
107
+ 'operate',
108
+ 'confirmation_required',
109
+ ['store', 'preview', 'checkout'],
110
+ ],
111
+ ['learn', 'learn', 'reversible', ['courses', 'study', 'progress']],
112
+ ['mail', 'focus', 'confirmation_required', ['inbox', 'compose', 'follow_up']],
113
+ ['meet', 'collaborate', 'confirmation_required', ['room', 'invite', 'meet']],
114
+ ['mind', 'learn', 'reversible', ['canvas', 'connect', 'organize']],
115
+ [
116
+ 'ai',
117
+ 'build',
118
+ 'confirmation_required',
119
+ ['playground', 'models', 'evaluate'],
120
+ ],
121
+ ['nova', 'build', 'confirmation_required', ['challenge', 'prompt', 'submit']],
122
+ ['tools', 'build', 'reversible', ['choose', 'configure', 'generate']],
123
+ ['rewise', 'learn', 'reversible', ['material', 'practice', 'review']],
124
+ ['shortener', 'build', 'reversible', ['link', 'customize', 'measure']],
125
+ ['tasks', 'focus', 'reversible', ['capture', 'plan', 'complete']],
126
+ ['teach', 'learn', 'confirmation_required', ['course', 'lesson', 'share']],
127
+ ['pay', 'operate', 'confirmation_required', ['request', 'review', 'pay']],
128
+ [
129
+ 'contacts',
130
+ 'operate',
131
+ 'confirmation_required',
132
+ ['people', 'fields', 'follow_up'],
133
+ ],
134
+ [
135
+ 'forms',
136
+ 'collaborate',
137
+ 'confirmation_required',
138
+ ['build', 'preview', 'share'],
139
+ ],
140
+ ['track', 'focus', 'reversible', ['timer', 'session', 'insights']],
141
+ ] as const satisfies readonly (readonly [
142
+ OnboardingAppSlug,
143
+ OnboardingGoal,
144
+ OnboardingActionSafety,
145
+ readonly [string, string, string],
146
+ ])[];
147
+
148
+ export const ONBOARDING_MISSIONS: readonly OnboardingMission[] = MISSIONS.map(
149
+ ([appSlug, goal, safety, capabilities]) => ({
150
+ appSlug,
151
+ capabilities,
152
+ goal,
153
+ safety,
154
+ })
155
+ );
156
+
157
+ export function getOnboardingMission(appSlug: string) {
158
+ return ONBOARDING_MISSIONS.find((mission) => mission.appSlug === appSlug);
159
+ }
160
+
161
+ export function recommendOnboardingApps(
162
+ preferences: OnboardingJourneyPreferences,
163
+ limit = 8
164
+ ): OnboardingAppSlug[] {
165
+ const ranked = new Map<string, number>();
166
+ const add = (slug: string, score: number) =>
167
+ ranked.set(slug, (ranked.get(slug) ?? 0) + score);
168
+
169
+ if (preferences.persona) {
170
+ PERSONA_RECOMMENDATIONS[preferences.persona].forEach((slug, index) => {
171
+ add(slug, 20 - index);
172
+ });
173
+ }
174
+
175
+ preferences.goals.forEach((goal) => {
176
+ GOAL_RECOMMENDATIONS[goal].forEach((slug, index) => {
177
+ add(slug, 12 - index);
178
+ });
179
+ });
180
+
181
+ const launchable = new Set(LAUNCHABLE_APPS.map((app) => app.slug));
182
+ return [...ranked.entries()]
183
+ .filter(([slug]) => launchable.has(slug as OnboardingAppSlug))
184
+ .sort((left, right) => right[1] - left[1])
185
+ .slice(0, limit)
186
+ .map(([slug]) => slug as OnboardingAppSlug);
187
+ }
188
+
189
+ export function createDefaultOnboardingProgress(): OnboardingJourneyProgress {
190
+ return {
191
+ completedMissionIds: [],
192
+ dismissedAt: null,
193
+ goals: [],
194
+ mode: 'standard',
195
+ persona: null,
196
+ replayApp: null,
197
+ revision: ONBOARDING_JOURNEY_REVISION,
198
+ };
199
+ }
@@ -7,7 +7,7 @@ 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.30.0'); // x-release-please-version
10
+ expect(TUTURUUU_PLATFORM_VERSION).toBe('0.32.0'); // x-release-please-version
11
11
  });
12
12
 
13
13
  it('preserves the source timestamp in builtAt and derives a short hash', () => {
@@ -1,6 +1,6 @@
1
1
  import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
2
2
 
3
- export const TUTURUUU_PLATFORM_VERSION = '0.30.0'; // x-release-please-version
3
+ export const TUTURUUU_PLATFORM_VERSION = '0.32.0'; // x-release-please-version
4
4
 
5
5
  export type PlatformBuildMetadataInput = {
6
6
  /** ISO timestamp of the source commit used for the build, not deployment time. */