@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 +14 -0
- package/biome.json +1 -1
- package/package.json +12 -12
- package/src/__tests__/api-proxy-guard.test.ts +29 -0
- package/src/api-proxy-guard.ts +5 -0
- package/src/launchable-apps.test.ts +11 -0
- package/src/launchable-apps.ts +23 -0
- package/src/next-config.test.ts +15 -0
- package/src/next-config.ts +8 -1
- package/src/onboarding.test.ts +35 -0
- package/src/onboarding.ts +199 -0
- package/src/platform-release.test.ts +1 -1
- package/src/platform-release.ts +1 -1
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
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tuturuuu/utils",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "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.
|
|
24
|
-
"@tiptap/react": "3.
|
|
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.
|
|
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.
|
|
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.
|
|
35
|
+
"effect": "^3.22.1",
|
|
36
36
|
"moment": "^2.30.1",
|
|
37
|
-
"next": "^16.3.0
|
|
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.
|
|
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.
|
|
52
|
-
"@types/react": "^19.2.
|
|
53
|
-
"@vitejs/plugin-react": "^6.0.
|
|
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.
|
|
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');
|
package/src/api-proxy-guard.ts
CHANGED
|
@@ -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');
|
package/src/launchable-apps.ts
CHANGED
|
@@ -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';
|
package/src/next-config.test.ts
CHANGED
|
@@ -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(
|
package/src/next-config.ts
CHANGED
|
@@ -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 ??
|
|
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.
|
|
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', () => {
|
package/src/platform-release.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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.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. */
|