@tuturuuu/utils 0.34.0 → 0.35.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,26 @@
1
+ import { FileKey2 } from '@tuturuuu/icons';
2
+ import type { RolePermission } from './permissions';
3
+
4
+ /** Called only inside the existing root/full infrastructure catalog branch. */
5
+ export function deploymentVaultPermissions(
6
+ t: (key: string) => string
7
+ ): RolePermission[] {
8
+ return [
9
+ {
10
+ id: 'manage_mobile_deployment_vault',
11
+ icon: <FileKey2 />,
12
+ title: t('ws-roles.manage_mobile_deployment_vault'),
13
+ description: t('ws-roles.manage_mobile_deployment_vault_description'),
14
+ disableOnProduction: false,
15
+ disabled: false,
16
+ },
17
+ {
18
+ id: 'manage_desktop_deployment_vault',
19
+ icon: <FileKey2 />,
20
+ title: t('ws-roles.manage_desktop_deployment_vault'),
21
+ description: t('ws-roles.manage_desktop_deployment_vault_description'),
22
+ disableOnProduction: false,
23
+ disabled: false,
24
+ },
25
+ ];
26
+ }
@@ -0,0 +1,20 @@
1
+ /** Shared pure signing allowlist; native CI imports this checkout file directly. */
2
+ export const DESKTOP_SIGNING_PROFILES = {
3
+ windows: {
4
+ files: ['windows_authenticode_certificate_pfx'],
5
+ scalars: ['WINDOWS_SIGNING_CERTIFICATE_PASSWORD'],
6
+ },
7
+ macos: {
8
+ files: [
9
+ 'macos_developer_id_certificate_p12',
10
+ 'macos_notarization_private_key_p8',
11
+ ],
12
+ scalars: [
13
+ 'MACOS_CERTIFICATE_PASSWORD',
14
+ 'MACOS_SIGNING_IDENTITY',
15
+ 'APPLE_TEAM_ID',
16
+ 'APP_STORE_CONNECT_API_KEY_ID',
17
+ 'APP_STORE_CONNECT_ISSUER_ID',
18
+ ],
19
+ },
20
+ } as const;
@@ -2,6 +2,7 @@ import type { NextConfig } from 'next';
2
2
  import { resolveInternalAppUrl } from './app-url';
3
3
  import { getLocalInternalAppUrl } from './internal-domains';
4
4
  import { getTuturuuuPortlessAllowedDevOrigins } from './portless';
5
+ import { createSeoHeaders, type SeoApp } from './seo';
5
6
 
6
7
  type Environment = Record<string, string | undefined>;
7
8
 
@@ -135,6 +136,10 @@ export function getTuturuuuNextOptimizePackageImports(
135
136
  }
136
137
 
137
138
  export interface TuturuuuNextConfigOptions extends NextConfig {
139
+ /** Explicit app policy; private routes receive an HTTP noindex directive. */
140
+ seoApp?: SeoApp;
141
+ /** Literal path regexes without a leading slash, for generated public pages. */
142
+ seoPublicPathPatterns?: readonly string[];
138
143
  /**
139
144
  * Path patterns (no leading slash, regex fragments) that this app allows to
140
145
  * be framed by third-party sites. Excluded from the platform-wide
@@ -150,7 +155,8 @@ export function createTuturuuuNextConfig(
150
155
  ): NextConfig {
151
156
  const experimentalConfig = config.experimental ?? {};
152
157
  const imageConfig = config.images ?? {};
153
- const { framablePathPatterns, ...nextConfig } = config;
158
+ const { framablePathPatterns, seoApp, seoPublicPathPatterns, ...nextConfig } =
159
+ config;
154
160
  const antiFramingSource = buildAntiFramingSource([
155
161
  ...TUTURUUU_DEFAULT_FRAMABLE_PATTERNS,
156
162
  ...(framablePathPatterns ?? []),
@@ -204,6 +210,7 @@ export function createTuturuuuNextConfig(
204
210
  headers: TUTURUUU_ANTI_FRAMING_HEADERS,
205
211
  },
206
212
  ...((await nextConfig.headers?.()) ?? []),
213
+ ...(seoApp ? createSeoHeaders(seoApp, seoPublicPathPatterns) : []),
207
214
  ];
208
215
  },
209
216
  };
@@ -53,6 +53,7 @@ import type { SupabaseUser } from '@tuturuuu/supabase/next/user';
53
53
  import type { PermissionId } from '@tuturuuu/types';
54
54
  import { ROOT_WORKSPACE_ID } from '@tuturuuu/utils/constants';
55
55
  import type { ReactNode } from 'react';
56
+ import { deploymentVaultPermissions } from './deployment-vault-permissions';
56
57
 
57
58
  export type RolePermission = {
58
59
  id: PermissionId;
@@ -142,16 +143,7 @@ export const permissionGroups = ({
142
143
  disableOnProduction: false,
143
144
  disabled: false,
144
145
  },
145
- {
146
- id: 'manage_mobile_deployment_vault' as PermissionId,
147
- icon: <FileKey2 />,
148
- title: t('ws-roles.manage_mobile_deployment_vault'),
149
- description: t(
150
- 'ws-roles.manage_mobile_deployment_vault_description'
151
- ),
152
- disableOnProduction: false,
153
- disabled: false,
154
- },
146
+ ...deploymentVaultPermissions(t),
155
147
  {
156
148
  id: 'manage_external_migrations',
157
149
  icon: <DatabaseZap />,
@@ -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.66.0'); // x-release-please-version
10
+ expect(TUTURUUU_PLATFORM_VERSION).toBe('0.67.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.66.0'; // x-release-please-version
3
+ export const TUTURUUU_PLATFORM_VERSION = '0.67.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. */
@@ -0,0 +1,29 @@
1
+ import { cache } from 'react';
2
+ import { getWorkspace } from './workspace-helper';
3
+
4
+ // React cache is scoped to a server render/request, never a shared authorization
5
+ // cache. Primitive actor keys let sibling layouts/pages reuse the same read.
6
+ const readWorkspace = cache(
7
+ (
8
+ id: string,
9
+ useAdmin: boolean,
10
+ actorId: string | null,
11
+ email: string | null
12
+ ) =>
13
+ getWorkspace(id, {
14
+ useAdmin,
15
+ ...(actorId ? { user: { id: actorId, email } } : {}),
16
+ })
17
+ );
18
+
19
+ export function getRequestWorkspace(
20
+ id: string,
21
+ options: Parameters<typeof getWorkspace>[1] = {}
22
+ ) {
23
+ return readWorkspace(
24
+ id,
25
+ options.useAdmin ?? false,
26
+ options.user?.id ?? null,
27
+ options.user?.email ?? null
28
+ );
29
+ }
@@ -0,0 +1,196 @@
1
+ {
2
+ "ai": {
3
+ "origin": "https://ai.tuturuuu.com",
4
+ "localePrefix": "never",
5
+ "publicPaths": [],
6
+ "publicPatterns": []
7
+ },
8
+ "apps": {
9
+ "origin": "https://apps.tuturuuu.com",
10
+ "localePrefix": "never",
11
+ "publicPaths": [""],
12
+ "publicPatterns": []
13
+ },
14
+ "calendar": {
15
+ "origin": "https://calendar.tuturuuu.com",
16
+ "localePrefix": "never",
17
+ "publicPaths": [],
18
+ "publicPatterns": []
19
+ },
20
+ "chat": {
21
+ "origin": "https://chat.tuturuuu.com",
22
+ "localePrefix": "never",
23
+ "publicPaths": [],
24
+ "publicPatterns": []
25
+ },
26
+ "cms": {
27
+ "origin": "https://cms.tuturuuu.com",
28
+ "localePrefix": "never",
29
+ "publicPaths": [],
30
+ "publicPatterns": []
31
+ },
32
+ "contacts": {
33
+ "origin": "https://contacts.tuturuuu.com",
34
+ "localePrefix": "never",
35
+ "publicPaths": [],
36
+ "publicPatterns": []
37
+ },
38
+ "drive": {
39
+ "origin": "https://drive.tuturuuu.com",
40
+ "localePrefix": "never",
41
+ "publicPaths": [],
42
+ "publicPatterns": []
43
+ },
44
+ "external": {
45
+ "origin": "https://external.tuturuuu.com",
46
+ "localePrefix": "never",
47
+ "publicPaths": [],
48
+ "publicPatterns": []
49
+ },
50
+ "finance": {
51
+ "origin": "https://finance.tuturuuu.com",
52
+ "localePrefix": "never",
53
+ "publicPaths": [],
54
+ "publicPatterns": []
55
+ },
56
+ "forms": {
57
+ "origin": "https://forms.tuturuuu.com",
58
+ "localePrefix": "never",
59
+ "publicPaths": [""],
60
+ "publicPatterns": ["f/[^/]+"]
61
+ },
62
+ "git": {
63
+ "origin": "https://git.tuturuuu.com",
64
+ "localePrefix": "never",
65
+ "publicPaths": ["tutur3u/platform"],
66
+ "publicPatterns": ["", "[^/]+/[^/]+(?:/.*)?"]
67
+ },
68
+ "hive": {
69
+ "origin": "https://hive.tuturuuu.com",
70
+ "localePrefix": "never",
71
+ "publicPaths": [],
72
+ "publicPatterns": []
73
+ },
74
+ "infrastructure": {
75
+ "origin": "https://infrastructure.tuturuuu.com",
76
+ "localePrefix": "never",
77
+ "publicPaths": [],
78
+ "publicPatterns": []
79
+ },
80
+ "inventory": {
81
+ "origin": "https://inventory.tuturuuu.com",
82
+ "localePrefix": "never",
83
+ "publicPaths": [],
84
+ "publicPatterns": []
85
+ },
86
+ "learn": {
87
+ "origin": "https://learn.tuturuuu.com",
88
+ "localePrefix": "never",
89
+ "publicPaths": [""],
90
+ "publicPatterns": []
91
+ },
92
+ "lettin": {
93
+ "origin": "https://lettin.tuturuuu.com",
94
+ "localePrefix": "never",
95
+ "publicPaths": ["", "worlds", "spaces"],
96
+ "publicPatterns": ["worlds/[^/]+", "creators/[^/]+"]
97
+ },
98
+ "mail": {
99
+ "origin": "https://mail.tuturuuu.com",
100
+ "localePrefix": "never",
101
+ "publicPaths": [],
102
+ "publicPatterns": []
103
+ },
104
+ "meet": {
105
+ "origin": "https://meet.tuturuuu.com",
106
+ "localePrefix": "never",
107
+ "publicPaths": [],
108
+ "publicPatterns": []
109
+ },
110
+ "mind": {
111
+ "origin": "https://mind.tuturuuu.com",
112
+ "localePrefix": "never",
113
+ "publicPaths": [],
114
+ "publicPatterns": []
115
+ },
116
+ "nova": {
117
+ "origin": "https://nova.tuturuuu.com",
118
+ "localePrefix": "as-needed",
119
+ "publicPaths": [
120
+ "",
121
+ "learn",
122
+ "learn/introduction",
123
+ "learn/basic-techniques",
124
+ "learn/advanced-strategies",
125
+ "learn/best-practices",
126
+ "competitions/neo-league/prompt-the-future/about"
127
+ ],
128
+ "publicPatterns": []
129
+ },
130
+ "parley": {
131
+ "origin": "https://parley.tuturuuu.com",
132
+ "localePrefix": "never",
133
+ "publicPaths": [],
134
+ "publicPatterns": []
135
+ },
136
+ "pay": {
137
+ "origin": "https://pay.tuturuuu.com",
138
+ "localePrefix": "never",
139
+ "publicPaths": [],
140
+ "publicPatterns": []
141
+ },
142
+ "playground": {
143
+ "origin": "https://playground.tuturuuu.com",
144
+ "localePrefix": "never",
145
+ "publicPaths": [],
146
+ "publicPatterns": []
147
+ },
148
+ "rewise": {
149
+ "origin": "https://rewise.tuturuuu.com",
150
+ "localePrefix": "never",
151
+ "publicPaths": [],
152
+ "publicPatterns": []
153
+ },
154
+ "shortener": {
155
+ "origin": "https://shortener.tuturuuu.com",
156
+ "localePrefix": "never",
157
+ "publicPaths": [],
158
+ "publicPatterns": []
159
+ },
160
+ "storefront": {
161
+ "origin": "https://storefront.tuturuuu.com",
162
+ "localePrefix": "never",
163
+ "publicPaths": [],
164
+ "publicPatterns": ["[^/]+", "[^/]+/products/[^/]+"]
165
+ },
166
+ "tasks": {
167
+ "origin": "https://tasks.tuturuuu.com",
168
+ "localePrefix": "never",
169
+ "publicPaths": [],
170
+ "publicPatterns": []
171
+ },
172
+ "teach": {
173
+ "origin": "https://teach.tuturuuu.com",
174
+ "localePrefix": "never",
175
+ "publicPaths": [""],
176
+ "publicPatterns": []
177
+ },
178
+ "tools": {
179
+ "origin": "https://tools.tuturuuu.com",
180
+ "localePrefix": "never",
181
+ "publicPaths": ["", "qr", "random"],
182
+ "publicPatterns": []
183
+ },
184
+ "track": {
185
+ "origin": "https://track.tuturuuu.com",
186
+ "localePrefix": "never",
187
+ "publicPaths": [],
188
+ "publicPatterns": []
189
+ },
190
+ "web": {
191
+ "origin": "https://tuturuuu.com",
192
+ "localePrefix": "as-needed",
193
+ "publicPaths": [],
194
+ "publicPatterns": []
195
+ }
196
+ }
@@ -0,0 +1,168 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { tryToParsePath } from 'next/dist/lib/try-to-parse-path';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { createTuturuuuNextConfig } from './next-config';
6
+ import { APP_SEO_POLICY, createSeoHeaders, type SeoApp } from './seo';
7
+
8
+ function blocked(
9
+ app: SeoApp,
10
+ pathname: string,
11
+ patterns?: string[],
12
+ preview = false
13
+ ) {
14
+ return createSeoHeaders(app, patterns, {
15
+ VERCEL_ENV: preview ? 'preview' : 'production',
16
+ }).some((rule) => {
17
+ const parsed = tryToParsePath(rule.source);
18
+ expect(parsed.error, rule.source).toBeUndefined();
19
+ return new RegExp(parsed.regexStr ?? '').test(pathname);
20
+ });
21
+ }
22
+
23
+ describe('app indexing boundaries', () => {
24
+ it.each(Object.keys(APP_SEO_POLICY) as SeoApp[])(
25
+ 'protects auth and API paths in %s',
26
+ (app) => {
27
+ for (const path of [
28
+ '/login',
29
+ '/vi/login',
30
+ '/en/verify-token',
31
+ '/api/v1/users',
32
+ '/dashboard',
33
+ '/invite/token',
34
+ ]) {
35
+ expect(blocked(app, path), `${app}: ${path}`).toBe(true);
36
+ }
37
+ }
38
+ );
39
+
40
+ it('keeps private apps, shared task links, meeting codes and embeds unindexed', () => {
41
+ for (const app of [
42
+ 'tasks',
43
+ 'meet',
44
+ 'parley',
45
+ 'chat',
46
+ 'ai',
47
+ 'infrastructure',
48
+ ] as const) {
49
+ for (const path of [
50
+ '/',
51
+ '/personal',
52
+ '/vi/workspace-id',
53
+ '/shared/task/secret',
54
+ '/r/secret',
55
+ ]) {
56
+ expect(blocked(app, path), `${app}: ${path}`).toBe(true);
57
+ }
58
+ }
59
+ expect(blocked('forms', '/embed/share-code')).toBe(true);
60
+ expect(blocked('forms', '/')).toBe(false);
61
+ expect(blocked('forms', '/f/share-code')).toBe(false);
62
+ expect(blocked('forms', '/f/share-code/responses')).toBe(true);
63
+ });
64
+
65
+ it('allows public shops and products but protects buyer transactions', () => {
66
+ for (const prefix of ['', '/vi', '/en']) {
67
+ expect(blocked('storefront', `${prefix}/my-store`)).toBe(false);
68
+ expect(blocked('storefront', `${prefix}/my-store/products/item`)).toBe(
69
+ false
70
+ );
71
+ for (const suffix of [
72
+ 'cart',
73
+ 'checkout',
74
+ 'checkout/success',
75
+ 'orders',
76
+ 'orders/bearer-token',
77
+ ]) {
78
+ expect(blocked('storefront', `${prefix}/my-store/${suffix}`)).toBe(
79
+ true
80
+ );
81
+ }
82
+ }
83
+ });
84
+
85
+ it('limits public education and tool surfaces to their declared pages', () => {
86
+ expect(blocked('learn', '/')).toBe(false);
87
+ expect(blocked('learn', '/personal/reports')).toBe(true);
88
+ expect(blocked('teach', '/personal/attendance')).toBe(true);
89
+ expect(blocked('tools', '/qr')).toBe(false);
90
+ expect(blocked('tools', '/qr/unknown')).toBe(true);
91
+ expect(blocked('nova', '/vi/learn/basic-techniques')).toBe(false);
92
+ expect(blocked('nova', '/vi/challenges')).toBe(true);
93
+ expect(blocked('git', '/-/personal/repositories')).toBe(true);
94
+ expect(blocked('git', '/tutur3u/platform/issues')).toBe(false);
95
+ expect(blocked('lettin', '/worlds/published-world')).toBe(false);
96
+ expect(blocked('lettin', '/personal/wiki/world')).toBe(true);
97
+ });
98
+
99
+ it('uses the generated platform allowlist and matches exact route boundaries', () => {
100
+ const paths: string[] = JSON.parse(
101
+ readFileSync(
102
+ resolve(
103
+ __dirname,
104
+ '../../../apps/web/src/lib/seo/public-routes.generated.json'
105
+ ),
106
+ 'utf8'
107
+ )
108
+ );
109
+ const patterns = [...paths.map((path) => path.slice(1)), 'changelog/[^/]+'];
110
+ for (const path of paths) {
111
+ expect(blocked('web', path, patterns), path).toBe(false);
112
+ expect(blocked('web', `/vi${path === '/' ? '' : path}`, patterns)).toBe(
113
+ false
114
+ );
115
+ }
116
+ for (const path of [
117
+ '/personal/tasks',
118
+ '/workspace-id',
119
+ '/documents/private-id',
120
+ '/share/document/secret',
121
+ '/ai/chats/private',
122
+ '/pricing/unknown',
123
+ ]) {
124
+ expect(blocked('web', path, patterns), path).toBe(true);
125
+ }
126
+ expect(blocked('web', '/changelog/released', patterns)).toBe(false);
127
+ });
128
+
129
+ it('noindexes all preview URLs and preserves the policy after app headers', async () => {
130
+ expect(blocked('tools', '/qr', undefined, true)).toBe(true);
131
+ const config = createTuturuuuNextConfig({
132
+ seoApp: 'learn',
133
+ headers: async () => [
134
+ {
135
+ source: '/login',
136
+ headers: [{ key: 'X-Robots-Tag', value: 'index' }],
137
+ },
138
+ ],
139
+ });
140
+ const rules = await config.headers?.();
141
+ expect(rules?.at(-1)?.headers[0]?.value).toBe(
142
+ 'noindex, nofollow, nosnippet'
143
+ );
144
+ expect(config).not.toHaveProperty('seoApp');
145
+ });
146
+
147
+ it('registers every Next app and prevents static/metadata route conflicts', () => {
148
+ const root = resolve(__dirname, '../../..');
149
+ for (const app of Object.keys(APP_SEO_POLICY)) {
150
+ const config = readFileSync(
151
+ resolve(root, `apps/${app}/next.config.ts`),
152
+ 'utf8'
153
+ );
154
+ expect(config).toContain(`seoApp: '${app}'`);
155
+ for (const file of ['robots', 'sitemap']) {
156
+ expect(
157
+ existsSync(resolve(root, `apps/${app}/src/app/${file}.ts`)) &&
158
+ existsSync(
159
+ resolve(
160
+ root,
161
+ `apps/${app}/public/${file === 'robots' ? 'robots.txt' : 'sitemap.xml'}`
162
+ )
163
+ )
164
+ ).toBe(false);
165
+ }
166
+ }
167
+ });
168
+ });
package/src/seo.ts ADDED
@@ -0,0 +1,49 @@
1
+ import policy from './seo-policy.json';
2
+
3
+ export type SeoApp = keyof typeof policy;
4
+ export const APP_SEO_POLICY = policy;
5
+ export const PRIVATE_ROBOTS_HEADER = 'noindex, nofollow, nosnippet';
6
+
7
+ // These remain private even where a public dynamic slug pattern could match.
8
+ const PRIVATE_PATH_PATTERN =
9
+ '(?:api|auth|login|logout|signup|verify-token|auth-error|add-account|dashboard|onboarding|invite|shared|share|embed|orders|cart|checkout|account|no-access|access-denied|not-whitelisted|not-available|-)(?:/|$)';
10
+
11
+ function escapeRegex(value: string) {
12
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
13
+ }
14
+
15
+ /** Next path-to-regexp fragments; every public pattern must match a whole URL. */
16
+ export function createSeoHeaders(
17
+ app: SeoApp,
18
+ publicPathPatterns?: readonly string[],
19
+ env: Record<string, string | undefined> = process.env
20
+ ) {
21
+ const appPolicy = policy[app];
22
+ const publicPatterns = publicPathPatterns ?? [
23
+ ...appPolicy.publicPaths.map(escapeRegex),
24
+ ...appPolicy.publicPatterns,
25
+ ];
26
+ const isPreview =
27
+ env.VERCEL_ENV === 'preview' ||
28
+ env.VERCEL_ENV === 'development' ||
29
+ env.NODE_ENV === 'development' ||
30
+ env.TUTURUUU_NOINDEX === '1';
31
+ const publicPattern = `(?:${publicPatterns.join('|')})/?$`;
32
+ const localePrefix = '(?:(?:en|vi)/)?';
33
+
34
+ return [
35
+ {
36
+ // A header reaches redirects, errors, API responses and non-HTML files.
37
+ // Preview builds and apps with no explicit public content fail closed.
38
+ source:
39
+ isPreview || publicPatterns.length === 0
40
+ ? '/:path*'
41
+ : `/:path((?!(?:(?:en|vi)(?:/|$))?${publicPattern}).*)`,
42
+ headers: [{ key: 'X-Robots-Tag', value: PRIVATE_ROBOTS_HEADER }],
43
+ },
44
+ {
45
+ source: `/:path(${localePrefix}${PRIVATE_PATH_PATTERN}.*)`,
46
+ headers: [{ key: 'X-Robots-Tag', value: PRIVATE_ROBOTS_HEADER }],
47
+ },
48
+ ];
49
+ }